From 254fa95eb5b3632f601200f4c9df816ae12ee8cb Mon Sep 17 00:00:00 2001 From: cchaos Date: Thu, 30 Jul 2020 16:53:05 -0400 Subject: [PATCH 01/13] POC: Allowing EuiFormRow props on EuiFieldText --- .../src/views/form_controls/field_text.js | 31 +++++++++++------ src/components/form/field_text/field_text.tsx | 33 +++++++++++++++++-- src/components/form/form_row/form_row.tsx | 6 ++-- 3 files changed, 55 insertions(+), 15 deletions(-) diff --git a/src-docs/src/views/form_controls/field_text.js b/src-docs/src/views/form_controls/field_text.js index 3369c564123..112c7916b8a 100644 --- a/src-docs/src/views/form_controls/field_text.js +++ b/src-docs/src/views/form_controls/field_text.js @@ -1,6 +1,6 @@ import React, { useState } from 'react'; -import { EuiFieldText } from '../../../../src/components'; +import { EuiFieldText, EuiSpacer } from '../../../../src/components'; import { DisplayToggles } from './display_toggles'; export default function() { @@ -11,14 +11,25 @@ export default function() { }; return ( - /* DisplayToggles wrapper for Docs only */ - - onChange(e)} - aria-label="Use aria labels when no actual label is in use" - /> - + <> + {/* DisplayToggles wrapper for Docs only */} + + onChange(e)} + aria-label="Use aria labels when no actual label is in use" + /> + + + + onChange(e)} + label="Use aria labels when no actual label is in use" + /> + + ); } diff --git a/src/components/form/field_text/field_text.tsx b/src/components/form/field_text/field_text.tsx index 12c7cc3d694..2e59fbf9650 100644 --- a/src/components/form/field_text/field_text.tsx +++ b/src/components/form/field_text/field_text.tsx @@ -27,8 +27,11 @@ import { } from '../form_control_layout'; import { EuiValidatableControl } from '../validatable_control'; +import { EuiFormRow } from '../form_row'; +import { EuiFormRowCommonProps } from '../form_row/form_row'; -export type EuiFieldTextProps = InputHTMLAttributes & +export type EuiFieldTextProps = Omit & + InputHTMLAttributes & CommonProps & { icon?: EuiFormControlLayoutProps['icon']; isInvalid?: boolean; @@ -77,6 +80,15 @@ export const EuiFieldText: FunctionComponent = ({ append, readOnly, controlOnly, + // FormRowProps + helpText, + error, + label, + labelAppend, + hasEmptyLabelSpace, + describedByIds, + display, + hasChildLabel, ...rest }) => { const classes = classNames('euiFieldText', className, { @@ -105,7 +117,7 @@ export const EuiFieldText: FunctionComponent = ({ if (controlOnly) return control; - return ( + const formControlLayout = ( = ({ {control} ); + + if (!label && !error && !helpText) return formControlLayout; + + const formRowProps = { + helpText, + error, + label, + labelAppend, + hasEmptyLabelSpace, + describedByIds: id ? [id] : undefined, + display: compressed ? 'rowCompressed' : display, + hasChildLabel, + fullWidth, + isInvalid, + }; + + return {formControlLayout}; }; diff --git a/src/components/form/form_row/form_row.tsx b/src/components/form/form_row/form_row.tsx index 19ddfda8f6b..2d5e556bd64 100644 --- a/src/components/form/form_row/form_row.tsx +++ b/src/components/form/form_row/form_row.tsx @@ -55,7 +55,7 @@ interface EuiFormRowState { id: string; } -type EuiFormRowCommonProps = CommonProps & { +export type EuiFormRowCommonProps = CommonProps & { /** * When `rowCompressed`, just tightens up the spacing; * Set to `columnCompressed` if compressed @@ -327,11 +327,11 @@ export class EuiFormRow extends Component { return labelType === 'legend' ? (
)}> + {...rest as HTMLAttributes}> {contents}
) : ( -
)}> +
}> {contents}
); From bd2a10afe4083040dc1d87ccfe4b5cb34318cad2 Mon Sep 17 00:00:00 2001 From: cchaos Date: Thu, 30 Jul 2020 16:56:40 -0400 Subject: [PATCH 02/13] Change label text --- src-docs/src/views/form_controls/field_text.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src-docs/src/views/form_controls/field_text.js b/src-docs/src/views/form_controls/field_text.js index 112c7916b8a..c1c41a2d564 100644 --- a/src-docs/src/views/form_controls/field_text.js +++ b/src-docs/src/views/form_controls/field_text.js @@ -27,7 +27,7 @@ export default function() { placeholder="Placeholder text" value={value} onChange={e => onChange(e)} - label="Use aria labels when no actual label is in use" + label="This text field has its own label prop" /> From 7d0e5d0609d611786e30ebb9fa7ccc873d907638 Mon Sep 17 00:00:00 2001 From: cchaos Date: Wed, 21 Oct 2020 18:40:03 -0400 Subject: [PATCH 03/13] WOrking through EuiFormRowProps --- src-docs/src/services/playground/knobs.js | 33 +++++++----- src-docs/src/views/button/playground.js | 3 +- .../src/views/form_controls/playground.js | 14 +++-- src/components/form/field_text/field_text.tsx | 52 ++++++++++++++----- src/components/form/form_row/form_row.tsx | 18 +++++-- 5 files changed, 80 insertions(+), 40 deletions(-) diff --git a/src-docs/src/services/playground/knobs.js b/src-docs/src/services/playground/knobs.js index 3094bf950cb..c6756ec7d8d 100644 --- a/src-docs/src/services/playground/knobs.js +++ b/src-docs/src/services/playground/knobs.js @@ -67,6 +67,10 @@ const Knob = ({ } }, [state, val, custom]); + if (hidden) { + return null; + } + let knobProps = {}; switch (type) { case PropTypes.Ref: @@ -104,8 +108,22 @@ const Knob = ({ ); + case PropTypes.ReactNode: case PropTypes.String: case PropTypes.Date: + if (name === 'children') { + return ( + { + set(e.target.value); + }} + /> + ); + } + if (custom && custom.validator) { knobProps = {}; knobProps.onChange = (e) => { @@ -137,7 +155,7 @@ const Knob = ({ helpText={custom && custom.helpText}> 0} compressed fullWidth @@ -219,19 +237,6 @@ const Knob = ({ ); } - case PropTypes.ReactNode: - if (name === 'children' && !hidden) { - return ( - { - set(e.target.value); - }} - /> - ); - } else return null; - case PropTypes.Custom: if (custom && custom.use) { switch (custom.use) { diff --git a/src-docs/src/views/button/playground.js b/src-docs/src/views/button/playground.js index 98891eda414..2b3ad69de97 100644 --- a/src-docs/src/views/button/playground.js +++ b/src-docs/src/views/button/playground.js @@ -16,8 +16,7 @@ export default () => { propsToUse.children = { value: 'Button', type: PropTypes.ReactNode, - description: 'Visible label.', - hidden: true, + description: 'Label of the button', }; propsToUse.minWidth = { diff --git a/src-docs/src/views/form_controls/playground.js b/src-docs/src/views/form_controls/playground.js index 178f891193a..eb3b2edb704 100644 --- a/src-docs/src/views/form_controls/playground.js +++ b/src-docs/src/views/form_controls/playground.js @@ -22,14 +22,12 @@ const fieldTextConfig = () => { : EuiFieldText.__docgenInfo; const propsToUse = propUtilityForPlayground(docgenInfo.props); - propsToUse.append = { - ...propsToUse.append, - type: PropTypes.String, - }; - propsToUse.prepend = { - ...propsToUse.prepend, - type: PropTypes.String, - }; + propsToUse.append.type = PropTypes.String; + propsToUse.prepend.type = PropTypes.String; + // propsToUse.label = { + // ...propsToUse.label, + // type: PropTypes.String, + // }; propsToUse.value = { ...propsToUse.value, diff --git a/src/components/form/field_text/field_text.tsx b/src/components/form/field_text/field_text.tsx index 2e59fbf9650..cceef071fb5 100644 --- a/src/components/form/field_text/field_text.tsx +++ b/src/components/form/field_text/field_text.tsx @@ -28,9 +28,20 @@ import { import { EuiValidatableControl } from '../validatable_control'; import { EuiFormRow } from '../form_row'; -import { EuiFormRowCommonProps } from '../form_row/form_row'; +import { + EuiFormRowCommonProps, + euiFormRowDisplayIsCompressed, +} from '../form_row/form_row'; + +type EuiFieldTextSupportedRowDisplays = + | 'row' + | 'rowCompressed' + | 'columnCompressed'; -export type EuiFieldTextProps = Omit & +export type EuiFieldTextProps = Omit< + EuiFormRowCommonProps, + 'children' | 'display' | 'hasChildLabel' +> & InputHTMLAttributes & CommonProps & { icon?: EuiFormControlLayoutProps['icon']; @@ -39,29 +50,36 @@ export type EuiFieldTextProps = Omit & 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 = ({ @@ -87,10 +105,12 @@ export const EuiFieldText: FunctionComponent = ({ labelAppend, hasEmptyLabelSpace, describedByIds, - display, - hasChildLabel, + display = 'row', ...rest }) => { + // Force compressed if `display` is compressed + compressed = euiFormRowDisplayIsCompressed(display) || compressed; + const classes = classNames('euiFieldText', className, { 'euiFieldText--withIcon': icon, 'euiFieldText--fullWidth': fullWidth, @@ -119,7 +139,15 @@ export const EuiFieldText: FunctionComponent = ({ const formControlLayout = ( = ({ ); - if (!label && !error && !helpText) return formControlLayout; + if (!label && !error && !helpText && !hasEmptyLabelSpace) + return formControlLayout; const formRowProps = { helpText, @@ -140,8 +169,7 @@ export const EuiFieldText: FunctionComponent = ({ labelAppend, hasEmptyLabelSpace, describedByIds: id ? [id] : undefined, - display: compressed ? 'rowCompressed' : display, - hasChildLabel, + display, fullWidth, isInvalid, }; diff --git a/src/components/form/form_row/form_row.tsx b/src/components/form/form_row/form_row.tsx index a0ac48aa4a9..97935d9622b 100644 --- a/src/components/form/form_row/form_row.tsx +++ b/src/components/form/form_row/form_row.tsx @@ -55,6 +55,12 @@ interface EuiFormRowState { id: string; } +export function euiFormRowDisplayIsCompressed( + display?: EuiFormRowDisplayKeys +): boolean { + return display ? display.includes('Compressed') : false; +} + export type EuiFormRowCommonProps = CommonProps & { /** * When `rowCompressed`, just tightens up the spacing; @@ -66,7 +72,6 @@ export 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` @@ -86,11 +91,16 @@ export type EuiFormRowCommonProps = CommonProps & { * being contained inside the form label. Good for things * like documentation links. */ - labelAppend?: any; + labelAppend?: ReactNode; id?: string; isInvalid?: boolean; error?: ReactNode | ReactNode[]; helpText?: ReactNode; + /** + * For use only in inline forms to align the inputs + * in case the form row has no label + */ + hasEmptyLabelSpace?: boolean; }; type LabelProps = { @@ -295,11 +305,11 @@ export class EuiFormRow extends Component { return labelType === 'legend' ? (
}> + {...(rest as HTMLAttributes)}> {contents}
) : ( -
}> +
)}> {contents}
); From ec4e4c7739a1580f8cb88750d46ece0b58c68c12 Mon Sep 17 00:00:00 2001 From: cchaos Date: Thu, 25 Feb 2021 16:56:56 -0500 Subject: [PATCH 04/13] Fixing up some prop names and playground --- .../playground/_playground_compiler.scss | 7 ++++++- .../src/views/form_controls/playground.js | 20 +++++++++++++++---- src/components/form/form_row/form_row.tsx | 7 +++++++ 3 files changed, 29 insertions(+), 5 deletions(-) diff --git a/src-docs/src/services/playground/_playground_compiler.scss b/src-docs/src/services/playground/_playground_compiler.scss index 79c32707828..cf4f6f530ee 100644 --- a/src-docs/src/services/playground/_playground_compiler.scss +++ b/src-docs/src/services/playground/_playground_compiler.scss @@ -1,4 +1,9 @@ .playgroundCompiler { + background: $euiColorEmptyShade; + position: sticky; + top: 0; + z-index: 5; + > div { justify-content: left !important; // sass-lint:disable-line no-important display: block !important; // sass-lint:disable-line no-important @@ -11,4 +16,4 @@ @if (lightness($euiTextColor) < 50) { background: $euiColorDarkestShade; } -} \ No newline at end of file +} diff --git a/src-docs/src/views/form_controls/playground.js b/src-docs/src/views/form_controls/playground.js index eb3b2edb704..6582b869217 100644 --- a/src-docs/src/views/form_controls/playground.js +++ b/src-docs/src/views/form_controls/playground.js @@ -24,10 +24,22 @@ const fieldTextConfig = () => { propsToUse.append.type = PropTypes.String; propsToUse.prepend.type = PropTypes.String; - // propsToUse.label = { - // ...propsToUse.label, - // type: PropTypes.String, - // }; + propsToUse.label = { + ...propsToUse.label, + type: PropTypes.String, + }; + propsToUse.labelAppend = { + ...propsToUse.labelAppend, + type: PropTypes.String, + }; + propsToUse.helpText = { + ...propsToUse.helpText, + type: PropTypes.String, + }; + propsToUse.error = { + ...propsToUse.error, + type: PropTypes.String, + }; propsToUse.value = { ...propsToUse.value, diff --git a/src/components/form/form_row/form_row.tsx b/src/components/form/form_row/form_row.tsx index 97935d9622b..d5eaee622ea 100644 --- a/src/components/form/form_row/form_row.tsx +++ b/src/components/form/form_row/form_row.tsx @@ -94,7 +94,14 @@ export type EuiFormRowCommonProps = CommonProps & { labelAppend?: ReactNode; id?: string; isInvalid?: boolean; + /** + * Error nodes will only show when `isInvalid` is true; + * Displayed after the input, before the `helpText` + */ error?: ReactNode | ReactNode[]; + /** + * Small text that displays below the input + */ helpText?: ReactNode; /** * For use only in inline forms to align the inputs From 97049c3fce496692ddadda6446bb87688533c448 Mon Sep 17 00:00:00 2001 From: cchaos Date: Sat, 27 Feb 2021 16:24:31 -0500 Subject: [PATCH 05/13] Playground sticky div --- src-docs/src/services/playground/playground.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src-docs/src/services/playground/playground.js b/src-docs/src/services/playground/playground.js index 503154b23be..a99d67684d9 100644 --- a/src-docs/src/services/playground/playground.js +++ b/src-docs/src/services/playground/playground.js @@ -54,7 +54,7 @@ export default ({ config, setGhostBackground, playgroundClassName }) => { ); return ( - +

{config.componentName}

@@ -74,7 +74,7 @@ export default ({ config, setGhostBackground, playgroundClassName }) => { - +
); }; From 47e3a0ca2d7bd620a66d40f36e6a763364a44904 Mon Sep 17 00:00:00 2001 From: cchaos Date: Sat, 27 Feb 2021 17:02:35 -0500 Subject: [PATCH 06/13] Added `labelProps` to EuiFormRow & EuiFieldText --- .../playground/_playground_compiler.scss | 2 +- src/components/form/field_text/field_text.tsx | 12 ++----- .../__snapshots__/form_row.test.tsx.snap | 31 +++++++++++++++++++ .../form/form_row/form_row.test.tsx | 10 ++++++ src/components/form/form_row/form_row.tsx | 7 +++++ 5 files changed, 52 insertions(+), 10 deletions(-) diff --git a/src-docs/src/services/playground/_playground_compiler.scss b/src-docs/src/services/playground/_playground_compiler.scss index cf4f6f530ee..515a7d9a1d2 100644 --- a/src-docs/src/services/playground/_playground_compiler.scss +++ b/src-docs/src/services/playground/_playground_compiler.scss @@ -1,7 +1,7 @@ .playgroundCompiler { background: $euiColorEmptyShade; position: sticky; - top: 0; + top: $euiHeaderHeightCompensation; z-index: 5; > div { diff --git a/src/components/form/field_text/field_text.tsx b/src/components/form/field_text/field_text.tsx index cceef071fb5..32dac744501 100644 --- a/src/components/form/field_text/field_text.tsx +++ b/src/components/form/field_text/field_text.tsx @@ -103,6 +103,7 @@ export const EuiFieldText: FunctionComponent = ({ error, label, labelAppend, + labelProps, hasEmptyLabelSpace, describedByIds, display = 'row', @@ -139,15 +140,7 @@ export const EuiFieldText: FunctionComponent = ({ const formControlLayout = ( = ({ error, label, labelAppend, + labelProps, hasEmptyLabelSpace, describedByIds: id ? [id] : undefined, display, 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 540698931b0..47bc0fe5491 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 @@ -574,3 +574,34 @@ exports[`EuiFormRow props label renders as a legend and subsquently a fieldset w
`; + +exports[`EuiFormRow props label with labelProps rendered 1`] = ` +
+
+ + label + +
+
+ +
+
+`; diff --git a/src/components/form/form_row/form_row.test.tsx b/src/components/form/form_row/form_row.test.tsx index 6c2cc893de7..84a26ad7d20 100644 --- a/src/components/form/form_row/form_row.test.tsx +++ b/src/components/form/form_row/form_row.test.tsx @@ -78,6 +78,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( diff --git a/src/components/form/form_row/form_row.tsx b/src/components/form/form_row/form_row.tsx index d5eaee622ea..16ca5454f19 100644 --- a/src/components/form/form_row/form_row.tsx +++ b/src/components/form/form_row/form_row.tsx @@ -86,6 +86,10 @@ export type EuiFormRowCommonProps = CommonProps & { */ children: ReactElement; label?: ReactNode; + /** + * Pass some common props to the `
`; exports[`EuiFieldText props compressed is rendered 1`] = ` - - - - - +
+ + + +
+ `; exports[`EuiFieldText props controlOnly is rendered 1`] = ` @@ -149,114 +133,158 @@ exports[`EuiFieldText props controlOnly 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 62a7ddc72f8..8b8e15c44bc 100644 --- a/src/components/form/field_text/field_text.test.tsx +++ b/src/components/form/field_text/field_text.test.tsx @@ -23,13 +23,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', })); @@ -106,7 +99,7 @@ describe('EuiFieldText', () => { }); test('append is rendered', () => { - const component = render(); + 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 ee929a4b40d..f1f0d0bfa30 100644 --- a/src/components/form/field_text/field_text.tsx +++ b/src/components/form/field_text/field_text.tsx @@ -21,10 +21,11 @@ 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'; @@ -109,11 +110,25 @@ export const EuiFieldText: FunctionComponent = ({ labelProps, hasEmptyLabelSpace, display = 'row', + 'aria-describedby': ariaDescribedBy, ...rest }) => { // 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 + ); + + // console.log(prependIDs); + // Force compressed if `display` is compressed compressed = euiFormRowDisplayIsCompressed(display) || compressed; @@ -136,6 +151,9 @@ export const EuiFieldText: FunctionComponent = ({ value={value} ref={inputRef} readOnly={readOnly} + aria-describedby={ + classNames(ariaDescribedBy, prependIDs, appendIDs) || undefined + } {...rest} /> @@ -144,17 +162,17 @@ export const EuiFieldText: FunctionComponent = ({ if (controlOnly) return control; const formControlLayout = ( - {control} - + ); if (!label && !error && !helpText && !hasEmptyLabelSpace) 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 2fdafdbf241..0114e022f3c 100644 --- a/src/components/form/form_control_layout/form_control_layout.tsx +++ b/src/components/form/form_control_layout/form_control_layout.tsx @@ -36,7 +36,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 & { @@ -44,12 +46,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']; @@ -118,7 +120,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..d732a6c6df7 --- /dev/null +++ b/src/components/form/form_control_layout/form_control_layout_updated.tsx @@ -0,0 +1,166 @@ +/* + * 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, + }); +}; From 004426d1b2cf9ae4864a0444126dfb2650d1d12c Mon Sep 17 00:00:00 2001 From: Caroline Horn <549577+cchaos@users.noreply.github.com> Date: Tue, 4 May 2021 11:37:35 -0400 Subject: [PATCH 12/13] Update src-docs/src/views/form_controls/form_controls_example.js --- src-docs/src/views/form_controls/form_controls_example.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 cfa4f339710..4506139dcab 100644 --- a/src-docs/src/views/form_controls/form_controls_example.js +++ b/src-docs/src/views/form_controls/form_controls_example.js @@ -195,7 +195,7 @@ export const FormControlsExample = { {' '} props directly. For instance, when providing the{' '} label prop, it will automically wrap itself - in a EuiFormRow. Other promoted props include{' '} + 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{' '} From d0a6440fcf439a5a593779e289e87c1b813d596f Mon Sep 17 00:00:00 2001 From: cchaos Date: Wed, 25 Aug 2021 18:34:54 -0400 Subject: [PATCH 13/13] Fix pass through of props --- src-docs/src/services/playground/knobs.js | 2 +- .../described_form_group.test.tsx.snap | 7 +++ .../__snapshots__/field_text.test.tsx.snap | 29 ++++++++++-- .../form/field_text/field_text.test.tsx | 7 +++ src/components/form/field_text/field_text.tsx | 12 +++-- .../__snapshots__/form_row.test.tsx.snap | 17 +++++++ .../form/form_row/form_row.test.tsx | 22 ++++++--- src/components/form/form_row/form_row.tsx | 47 ++++++++++++------- 8 files changed, 110 insertions(+), 33 deletions(-) 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/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} >
@@ -23,7 +23,6 @@ exports[`EuiFieldText form row props are rendered 1`] = ` class="euiFormRow__fieldWrapper" >
@@ -43,13 +43,13 @@ exports[`EuiFieldText form row props are rendered 1`] = `
error
helpText
@@ -132,6 +132,25 @@ exports[`EuiFieldText props controlOnly is rendered 1`] = ` `; +exports[`EuiFieldText props disabled is rendered 1`] = ` +
+
+ + + +
+
+`; + exports[`EuiFieldText props fullWidth is rendered 1`] = `
{ expect(component).toMatchSnapshot(); }); + test('disabled is rendered', () => { + const component = render(); + + expect(component).toMatchSnapshot(); + }); + test('compressed is rendered', () => { const component = render(); @@ -107,6 +113,7 @@ describe('EuiFieldText', () => { display="columnCompressed" fullWidth={true} isInvalid={true} + isDisabled={true} /> ); diff --git a/src/components/form/field_text/field_text.tsx b/src/components/form/field_text/field_text.tsx index 14abc2ee101..2d804326d26 100644 --- a/src/components/form/field_text/field_text.tsx +++ b/src/components/form/field_text/field_text.tsx @@ -91,6 +91,7 @@ export const EuiFieldText: FunctionComponent = ({ append, readOnly, controlOnly, + disabled, // FormRowProps helpText, error, @@ -100,8 +101,12 @@ export const EuiFieldText: FunctionComponent = ({ 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')(); @@ -116,8 +121,6 @@ export const EuiFieldText: FunctionComponent = ({ append ); - // console.log(prependIDs); - // Force compressed if `display` is compressed compressed = euiFormRowDisplayIsCompressed(display) || compressed; @@ -140,6 +143,7 @@ export const EuiFieldText: FunctionComponent = ({ value={value} ref={inputRef} readOnly={readOnly} + disabled={isDisabled} aria-describedby={ classNames(ariaDescribedBy, prependIDs, appendIDs) || undefined } @@ -169,7 +173,7 @@ export const EuiFieldText: FunctionComponent = ({ return formControlLayout; const formRowProps = { - inputId: id, + id, helpText, error, label, @@ -179,6 +183,8 @@ export const EuiFieldText: FunctionComponent = ({ display, fullWidth, isInvalid, + isDisabled, + passThrough: false, }; return {formControlLayout}; 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 b90b509eaed..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 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 40a88353d6e..3c40265f495 100644 --- a/src/components/form/form_row/form_row.test.tsx +++ b/src/components/form/form_row/form_row.test.tsx @@ -65,7 +65,7 @@ describe('EuiFormRow', () => { }; const tree = shallow( - + ); @@ -75,17 +75,15 @@ describe('EuiFormRow', () => { expect(tree.find('EuiFormLabel').prop('htmlFor')).toEqual('inputId'); // Input is described by help and error text. - expect(tree.find('EuiFormHelpText').prop('id')).toEqual( - 'generated-id-help-0' - ); + expect(tree.find('EuiFormHelpText').prop('id')).toEqual('inputId-help-0'); expect(tree.find('EuiFormErrorText').at(0).prop('id')).toEqual( - 'generated-id-error-0' + 'inputId-error-0' ); expect(tree.find('EuiFormErrorText').at(1).prop('id')).toEqual( - 'generated-id-error-1' + 'inputId-error-1' ); expect(tree.find('input').prop('aria-describedby')).toEqual( - 'generated-id-help-0 generated-id-error-0 generated-id-error-1' + 'inputId-help-0 inputId-error-0 inputId-error-1' ); }); @@ -252,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 918ffe4d273..00267b03f41 100644 --- a/src/components/form/form_row/form_row.tsx +++ b/src/components/form/form_row/form_row.tsx @@ -89,15 +89,15 @@ export type EuiFormRowCommonProps = CommonProps & { */ labelAppend?: ReactNode; /** - * Used to generate the wrapper `id`s and will be passed plainly to the child (input) if `inputId` is not supplied. + * Used to generate the wrapper `id`s and will be passed plainly + * to the child (input) if `inputId` is not supplied. + * * If not provided, one will be generated */ id?: string; /** - * Specific `id` passed to the label as the `htmlFor` attribute. - * If not provided, one will be generated + * Shows `error` list and passed to label element */ - inputId?: string; isInvalid?: boolean; /** * Error nodes will only show when `isInvalid` is true; @@ -114,7 +114,8 @@ export type EuiFormRowCommonProps = CommonProps & { */ hasEmptyLabelSpace?: boolean; /** - * Passed along to the label element; and to the child field element when `disabled` doesn't already exist on the child field element. + * Passed along to the label element; and to the child field element + * when `disabled` doesn't already exist on the child field element. */ isDisabled?: boolean; }; @@ -133,7 +134,14 @@ type LegendProps = { } & EuiFormRowCommonProps & Omit, 'disabled'>; -export type EuiFormRowProps = ExclusiveUnion; +export type EuiFormRowProps = ExclusiveUnion & { + /** + * **INTERNAL** + * Whether to pass clone and pass through certain props to the child. + * Set to `false` when used within a form control itself. + */ + passThrough?: boolean; +}; export class EuiFormRow extends Component { static defaultProps = { @@ -143,6 +151,7 @@ export class EuiFormRow extends Component { describedByIds: [], labelType: 'label', hasChildLabel: true, + passThrough: true, }; state: EuiFormRowState = { @@ -197,8 +206,8 @@ export class EuiFormRow extends Component { display, hasChildLabel, id: propsId, - inputId, isDisabled, + passThrough, ...rest } = this.props; @@ -263,7 +272,7 @@ export class EuiFormRow extends Component { } else { labelProps = { ..._labelProps, - htmlFor: hasChildLabel ? inputId || id : undefined, + htmlFor: hasChildLabel ? id : undefined, isFocused: this.state.isFocused, ...(!isDisabled && { isFocused: this.state.isFocused }), // If the row is disabled, don't pass the isFocused state. type: labelType, @@ -306,15 +315,19 @@ export class EuiFormRow extends Component { optionalProps['aria-describedby'] = describingIds.join(' '); } - const field = cloneElement(Children.only(children), { - id: inputId ? undefined : id, - disabled: isDisabled, - // Allow the child's disabled or isDisabled prop to supercede the ones before - ...children.props, - onFocus: this.onFocus, - onBlur: this.onBlur, - ...optionalProps, - }); + const child = Children.only(children); + // Only clone the element and pass through props when not used directly within a form control + const field = passThrough + ? cloneElement(child, { + id, + // Allow the child's disabled or isDisabled prop to supercede the `isDisabled` + disabled: + child.props.disabled ?? child.props.isDisabled ?? isDisabled, + onFocus: this.onFocus, + onBlur: this.onBlur, + ...optionalProps, + }) + : child; const fieldWrapperClasses = classNames('euiFormRow__fieldWrapper', { euiFormRow__fieldWrapperDisplayOnly: