diff --git a/scripts/jest/setup/throw_on_console_error.js b/scripts/jest/setup/throw_on_console_error.js index c6ab41954c1..c6d2464bdbe 100644 --- a/scripts/jest/setup/throw_on_console_error.js +++ b/scripts/jest/setup/throw_on_console_error.js @@ -1,6 +1,8 @@ +import { format } from 'util' + // Fail if a test ends up `console.error`-ing, e.g. if React logs because of a // failed prop types check. -console.error = (message) => { +console.error = (message, ...rest) => { // @see https://github.com/emotion-js/emotion/issues/1105 // This error that Emotion throws doesn't apply to Jest, so // we're just going to straight up ignore the first/nth-child warning @@ -14,5 +16,5 @@ console.error = (message) => { return; } - throw new Error(message); + throw new Error(format(message, ...rest)); }; 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 2e697ecb05d..61e1df7912b 100644 --- a/src-docs/src/views/form_layouts/form_layouts_example.js +++ b/src-docs/src/views/form_layouts/form_layouts_example.js @@ -25,6 +25,9 @@ const describedFormGroupRatioSource = require('!!raw-loader!./described_form_gro import FullWidth from './full_width'; const fullWidthSource = require('!!raw-loader!./full_width'); +import FullWidthViaContext from './full_width_via_context'; +const fullWidthViaContextSource = require('!!raw-loader!./full_width_via_context'); + import Inline from './inline'; const inlineSource = require('!!raw-loader!./inline'); @@ -130,6 +133,31 @@ export const FormLayoutsExample = { > `, + }, + { + title: 'Global full-width', + text: ( +

+ To set all the row and controls in a form to{' '} + fullWidth, specify the prop on the root{' '} + EuiForm component. +

+ ), + props: { + EuiForm, + }, + demo: , + source: [ + { + type: GuideSectionTypes.JS, + code: fullWidthViaContextSource, + }, + ], + snippet: ` + + + +`, }, { title: 'Inline', diff --git a/src-docs/src/views/form_layouts/full_width_via_context.tsx b/src-docs/src/views/form_layouts/full_width_via_context.tsx new file mode 100644 index 00000000000..ba85b140bd3 --- /dev/null +++ b/src-docs/src/views/form_layouts/full_width_via_context.tsx @@ -0,0 +1,101 @@ +import React from 'react'; + +import { + EuiForm, + EuiFieldSearch, + EuiRange, + EuiTextArea, + EuiFormRow, + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, + EuiDescribedFormGroup, + EuiSelect, + EuiFilePicker, + EuiButton, +} from '../../../../src/components'; + +export default () => { + const [range, setRange] = React.useState(42); + + return ( + { + e.preventDefault(); + }} + > + + + + + + Search + + + + + + + { + if (e.target instanceof HTMLInputElement) { + setRange(Number.parseInt(e.target.value, 10)); + } + }} + /> + + + + + Works with all form controls and layout components} + description={ + <> +

+ Any component that supports the fullWidth prop that + is. +

+

+ Make sure it is appropriate at all of the widths that the + container can take. There are many situations where a full-width + form is inappropriate. +

+ + } + > + + + +
+ + + + + + + + +
+ ); +}; 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 1da4b5d8df1..8f9315731a9 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 @@ -40,7 +40,6 @@ exports[`EuiDescribedFormGroup is rendered 1`] = ` { expect(component).toMatchSnapshot(); }); }); + + describe('inherits', () => { + test('fullWidth from ', () => { + const component = render( + + + + ); + + if ( + !component + .find('.euiDescribedFormGroup') + .hasClass('euiDescribedFormGroup--fullWidth') + ) { + throw new Error( + 'expected EuiDescribedFormGroup to inherit fullWidth from EuiForm' + ); + } + }); + }); }); diff --git a/src/components/form/described_form_group/described_form_group.tsx b/src/components/form/described_form_group/described_form_group.tsx index 41e92631afd..5546587adc4 100644 --- a/src/components/form/described_form_group/described_form_group.tsx +++ b/src/components/form/described_form_group/described_form_group.tsx @@ -20,6 +20,7 @@ import { EuiFlexGroupGutterSize, EuiFlexItemProps, } from '../../flex'; +import { useFormContext } from '../eui_form_context'; export type EuiDescribedFormGroupProps = CommonProps & Omit, 'title'> & { @@ -29,17 +30,21 @@ export type EuiDescribedFormGroupProps = CommonProps & children?: ReactNode; /** * Passed to `EuiFlexGroup`. + * @default l */ gutterSize?: EuiFlexGroupGutterSize; /** * Expand to fill 100% of the parent. + * Defaults to `fullWidth` prop of ``. * Default max-width is 800px. + * @default false */ fullWidth?: boolean; /** * Width ratio of description column compared to field column. * Can be used in conjunction with `fullWidth` and * may require `fullWidth` to be applied to child elements. + * @default half */ ratio?: 'half' | 'third' | 'quarter'; /** @@ -48,6 +53,7 @@ export type EuiDescribedFormGroupProps = CommonProps & title: EuiTitleProps['children']; /** * Adjust the visual `size` of the EuiTitle that wraps `title`. + * @default xs */ titleSize?: EuiTitleSize; /** @@ -64,19 +70,24 @@ export type EuiDescribedFormGroupProps = CommonProps & fieldFlexItemProps?: PropsOf; }; -export const EuiDescribedFormGroup: FunctionComponent = ({ - children, - className, - gutterSize = 'l', - fullWidth = false, - ratio = 'half', - titleSize = 'xs', - title, - description, - descriptionFlexItemProps, - fieldFlexItemProps, - ...rest -}) => { +export const EuiDescribedFormGroup: FunctionComponent = ( + props +) => { + const { defaultFullWidth } = useFormContext(); + + const { + children, + className, + gutterSize = 'l', + fullWidth = defaultFullWidth, + ratio = 'half', + titleSize = 'xs', + title, + description, + descriptionFlexItemProps, + fieldFlexItemProps, + ...rest + } = props; const classes = classNames( 'euiDescribedFormGroup', { @@ -93,18 +104,16 @@ export const EuiDescribedFormGroup: FunctionComponent{description}

; - } - renderedDescription = ( - {description} + { + // If the description is just a string, wrap it in a paragraph element + typeof description === 'string' ?

{description}

: description + }
); } diff --git a/src/components/form/eui_form_context.ts b/src/components/form/eui_form_context.ts new file mode 100644 index 00000000000..2b523d128a5 --- /dev/null +++ b/src/components/form/eui_form_context.ts @@ -0,0 +1,21 @@ +/* + * 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. + */ + +import React from 'react'; + +export interface FormContextValue { + defaultFullWidth: boolean; +} + +export const FormContext = React.createContext({ + defaultFullWidth: false, +}); + +export function useFormContext() { + return React.useContext(FormContext); +} diff --git a/src/components/form/field_number/field_number.test.tsx b/src/components/form/field_number/field_number.test.tsx index 1a9e5ab2f6f..16b0fcb583e 100644 --- a/src/components/form/field_number/field_number.test.tsx +++ b/src/components/form/field_number/field_number.test.tsx @@ -10,6 +10,7 @@ import React from 'react'; import { render } from 'enzyme'; import { requiredProps } from '../../../test/required_props'; +import { EuiForm } from '../form'; import { EuiFieldNumber } from './field_number'; jest.mock('../form_control_layout', () => { @@ -89,4 +90,22 @@ describe('EuiFieldNumber', () => { }); }); }); + + describe('inherits', () => { + test('fullWidth from ', () => { + const component = render( + + + + ); + + if ( + !component.find('.euiFieldNumber').hasClass('euiFieldNumber--fullWidth') + ) { + throw new Error( + 'expected EuiFieldNumber to inherit fullWidth from EuiForm' + ); + } + }); + }); }); diff --git a/src/components/form/field_number/field_number.tsx b/src/components/form/field_number/field_number.tsx index c9dc16e25c8..777e287f379 100644 --- a/src/components/form/field_number/field_number.tsx +++ b/src/components/form/field_number/field_number.tsx @@ -18,6 +18,7 @@ import { import { EuiValidatableControl } from '../validatable_control'; import { IconType } from '../../icon'; +import { useFormContext } from '../eui_form_context'; import { getFormControlClassNameForIconCount } from '../form_control_layout/_num_icons'; export type EuiFieldNumberProps = Omit< @@ -27,7 +28,15 @@ export type EuiFieldNumberProps = Omit< CommonProps & { icon?: IconType; isInvalid?: boolean; + /** + * Expand to fill 100% of the parent. + * Defaults to `fullWidth` prop of ``. + * @default false + */ fullWidth?: boolean; + /** + * @default false + */ isLoading?: boolean; readOnly?: boolean; min?: number; @@ -60,30 +69,36 @@ export type EuiFieldNumberProps = Omit< /** * when `true` creates a shorter height input + * @default false */ compressed?: boolean; }; -export const EuiFieldNumber: FunctionComponent = ({ - className, - icon, - id, - placeholder, - name, - min, - max, - value, - isInvalid, - fullWidth = false, - isLoading = false, - compressed = false, - prepend, - append, - inputRef, - readOnly, - controlOnly, - ...rest -}) => { +export const EuiFieldNumber: FunctionComponent = ( + props +) => { + const { defaultFullWidth } = useFormContext(); + const { + className, + icon, + id, + placeholder, + name, + min, + max, + value, + isInvalid, + fullWidth = defaultFullWidth, + isLoading = false, + compressed = false, + prepend, + append, + inputRef, + readOnly, + controlOnly, + ...rest + } = props; + const numIconsClass = getFormControlClassNameForIconCount({ isInvalid, isLoading, diff --git a/src/components/form/field_password/field_password.test.tsx b/src/components/form/field_password/field_password.test.tsx index 4247d702346..31db574e42f 100644 --- a/src/components/form/field_password/field_password.test.tsx +++ b/src/components/form/field_password/field_password.test.tsx @@ -11,6 +11,7 @@ import { render, mount } from 'enzyme'; import { requiredProps } from '../../../test/required_props'; import { shouldRenderCustomStyles } from '../../../test/internal'; +import { EuiForm } from '../form'; import { EuiFieldPassword, EuiFieldPasswordProps } from './field_password'; jest.mock('../validatable_control', () => ({ @@ -140,4 +141,24 @@ describe('EuiFieldPassword', () => { }); }); }); + + describe('inherits', () => { + test('fullWidth from ', () => { + const component = render( + + + + ); + + if ( + !component + .find('.euiFieldPassword') + .hasClass('euiFieldPassword--fullWidth') + ) { + throw new Error( + 'expected EuiFieldPassword to inherit fullWidth from EuiForm' + ); + } + }); + }); }); diff --git a/src/components/form/field_password/field_password.tsx b/src/components/form/field_password/field_password.tsx index 14ec5c65d8e..461385f1558 100644 --- a/src/components/form/field_password/field_password.tsx +++ b/src/components/form/field_password/field_password.tsx @@ -25,6 +25,7 @@ import { EuiButtonIcon, EuiButtonIconPropsForButton } from '../../button'; import { useEuiI18n } from '../../i18n'; import { useCombinedRefs } from '../../../services'; import { getFormControlClassNameForIconCount } from '../form_control_layout/_num_icons'; +import { useFormContext } from '../eui_form_context'; export type EuiFieldPasswordProps = Omit< InputHTMLAttributes, @@ -32,6 +33,11 @@ export type EuiFieldPasswordProps = Omit< > & CommonProps & { isInvalid?: boolean; + /** + * Expand to fill 100% of the parent. + * Defaults to `fullWidth` prop of ``. + * @default false + */ fullWidth?: boolean; isLoading?: boolean; compressed?: boolean; @@ -54,6 +60,7 @@ export type EuiFieldPasswordProps = Omit< * Change the `type` of input for manually handling obfuscation. * The `dual` option adds the ability to toggle the obfuscation of the input by * adding an icon button as the first `append` element + * @default password */ type?: 'password' | 'text' | 'dual'; @@ -63,23 +70,28 @@ export type EuiFieldPasswordProps = Omit< dualToggleProps?: Partial; }; -export const EuiFieldPassword: FunctionComponent = ({ - className, - id, - name, - placeholder, - value, - isInvalid, - fullWidth, - isLoading, - compressed, - inputRef: _inputRef, - prepend, - append, - type = 'password', - dualToggleProps, - ...rest -}) => { +export const EuiFieldPassword: FunctionComponent = ( + props +) => { + const { defaultFullWidth } = useFormContext(); + const { + className, + id, + name, + placeholder, + value, + isInvalid, + fullWidth = defaultFullWidth, + isLoading, + compressed, + inputRef: _inputRef, + prepend, + append, + type = 'password', + dualToggleProps, + ...rest + } = props; + // Set the initial input type to `password` if they want dual const [inputType, setInputType] = useState( type === 'dual' ? 'password' : type @@ -182,7 +194,6 @@ export const EuiFieldPassword: FunctionComponent = ({ EuiFieldPassword.defaultProps = { value: undefined, - fullWidth: false, isLoading: false, compressed: false, }; diff --git a/src/components/form/field_search/field_search.test.tsx b/src/components/form/field_search/field_search.test.tsx index 770af86f7c8..b818497352a 100644 --- a/src/components/form/field_search/field_search.test.tsx +++ b/src/components/form/field_search/field_search.test.tsx @@ -10,6 +10,7 @@ import React from 'react'; import { render } from 'enzyme'; import { requiredProps } from '../../../test/required_props'; +import { EuiForm } from '../form'; import { EuiFieldSearch } from './field_search'; jest.mock('../form_control_layout', () => ({ @@ -82,4 +83,22 @@ describe('EuiFieldSearch', () => { expect(component).toMatchSnapshot(); }); }); + + describe('inherits', () => { + test('fullWidth from ', () => { + const component = render( + + + + ); + + if ( + !component.find('.euiFieldSearch').hasClass('euiFieldSearch--fullWidth') + ) { + throw new Error( + 'expected EuiFieldSearch to inherit fullWidth from EuiForm' + ); + } + }); + }); }); diff --git a/src/components/form/field_search/field_search.tsx b/src/components/form/field_search/field_search.tsx index 0ddf39de8a2..b3c9bc595ef 100644 --- a/src/components/form/field_search/field_search.tsx +++ b/src/components/form/field_search/field_search.tsx @@ -19,6 +19,7 @@ import { import { EuiValidatableControl } from '../validatable_control'; import { getFormControlClassNameForIconCount } from '../form_control_layout/_num_icons'; +import { FormContext, FormContextValue } from '../eui_form_context'; export interface EuiFieldSearchProps extends CommonProps, @@ -28,6 +29,11 @@ export interface EuiFieldSearchProps placeholder?: string; value?: string; isInvalid?: boolean; + /** + * Expand to fill 100% of the parent. + * Defaults to `fullWidth` prop of ``. + * @default false + */ fullWidth?: boolean; isLoading?: boolean; /** @@ -72,8 +78,8 @@ export class EuiFieldSearch extends Component< EuiFieldSearchProps, EuiFieldSearchState > { + static contextType = FormContext; static defaultProps = { - fullWidth: false, isLoading: false, incremental: false, compressed: false, @@ -197,13 +203,14 @@ export class EuiFieldSearch extends Component< }; render() { + const { defaultFullWidth } = this.context as FormContextValue; const { className, id, name, placeholder, isInvalid, - fullWidth, + fullWidth = defaultFullWidth, isLoading, inputRef, incremental, diff --git a/src/components/form/field_text/field_text.test.tsx b/src/components/form/field_text/field_text.test.tsx index 86fae243ef1..3928b38a50b 100644 --- a/src/components/form/field_text/field_text.test.tsx +++ b/src/components/form/field_text/field_text.test.tsx @@ -10,6 +10,7 @@ import React from 'react'; import { render } from 'enzyme'; import { requiredProps } from '../../../test/required_props'; +import { EuiForm } from '../form'; import { EuiFieldText } from './field_text'; jest.mock('../form_control_layout', () => { @@ -72,4 +73,22 @@ describe('EuiFieldText', () => { expect(component).toMatchSnapshot(); }); }); + + describe('inherits', () => { + test('fullWidth from ', () => { + const component = render( + + + + ); + + if ( + !component.find('.euiFieldText').hasClass('euiFieldText--fullWidth') + ) { + throw new Error( + 'expected EuiFieldText to inherit fullWidth from EuiForm' + ); + } + }); + }); }); diff --git a/src/components/form/field_text/field_text.tsx b/src/components/form/field_text/field_text.tsx index 2b38745560e..5c75564de71 100644 --- a/src/components/form/field_text/field_text.tsx +++ b/src/components/form/field_text/field_text.tsx @@ -17,11 +17,17 @@ import { import { EuiValidatableControl } from '../validatable_control'; import { getFormControlClassNameForIconCount } from '../form_control_layout/_num_icons'; +import { useFormContext } from '../eui_form_context'; export type EuiFieldTextProps = InputHTMLAttributes & CommonProps & { icon?: EuiFormControlLayoutProps['icon']; isInvalid?: boolean; + /** + * Expand to fill 100% of the parent. + * Defaults to `fullWidth` prop of ``. + * @default false + */ fullWidth?: boolean; isLoading?: boolean; readOnly?: boolean; @@ -51,24 +57,27 @@ export type EuiFieldTextProps = InputHTMLAttributes & compressed?: boolean; }; -export const EuiFieldText: FunctionComponent = ({ - id, - name, - placeholder, - value, - className, - icon, - isInvalid, - inputRef, - fullWidth = false, - isLoading, - compressed, - prepend, - append, - readOnly, - controlOnly, - ...rest -}) => { +export const EuiFieldText: FunctionComponent = (props) => { + const { defaultFullWidth } = useFormContext(); + const { + id, + name, + placeholder, + value, + className, + icon, + isInvalid, + inputRef, + fullWidth = defaultFullWidth, + isLoading, + compressed, + prepend, + append, + readOnly, + controlOnly, + ...rest + } = props; + const numIconsClass = getFormControlClassNameForIconCount({ isInvalid, isLoading, diff --git a/src/components/form/file_picker/file_picker.test.tsx b/src/components/form/file_picker/file_picker.test.tsx index 2c8f5002376..a199771570d 100644 --- a/src/components/form/file_picker/file_picker.test.tsx +++ b/src/components/form/file_picker/file_picker.test.tsx @@ -10,6 +10,7 @@ import React from 'react'; import { render } from 'enzyme'; import { requiredProps } from '../../../test'; +import { EuiForm } from '../form'; import { EuiFilePicker } from './file_picker'; describe('EuiFilePicker', () => { @@ -18,4 +19,22 @@ describe('EuiFilePicker', () => { expect(component).toMatchSnapshot(); }); + + describe('inherits', () => { + test('fullWidth from ', () => { + const component = render( + + + + ); + + if ( + !component.find('.euiFilePicker').hasClass('euiFilePicker--fullWidth') + ) { + throw new Error( + 'expected EuiFilePicker to inherit fullWidth from EuiForm' + ); + } + }); + }); }); diff --git a/src/components/form/file_picker/file_picker.tsx b/src/components/form/file_picker/file_picker.tsx index 6fe56f6f16c..ea68af5751d 100644 --- a/src/components/form/file_picker/file_picker.tsx +++ b/src/components/form/file_picker/file_picker.tsx @@ -18,6 +18,7 @@ import { EuiIcon } from '../../icon'; import { EuiI18n } from '../../i18n'; import { EuiLoadingSpinner } from '../../loading'; import { htmlIdGenerator } from '../../../services/accessibility'; +import { FormContext, FormContextValue } from '../eui_form_context'; const displayToClassNameMap = { default: null, @@ -52,6 +53,11 @@ export interface EuiFilePickerProps * `large` for taller size */ display?: EuiFilePickerDisplay; + /** + * Expand to fill 100% of the parent. + * Defaults to `fullWidth` prop of ``. + * @default false + */ fullWidth?: boolean; isInvalid?: boolean; isLoading?: boolean; @@ -59,6 +65,8 @@ export interface EuiFilePickerProps } export class EuiFilePicker extends Component { + static contextType = FormContext; + static defaultProps = { initialPromptText: ( { }; render() { + const { defaultFullWidth } = this.context as FormContextValue; + return ( { compressed, onChange, isInvalid, - fullWidth, + fullWidth = defaultFullWidth, isLoading, display, ...rest diff --git a/src/components/form/form.tsx b/src/components/form/form.tsx index 3944cf4dd54..a3d653f72d1 100644 --- a/src/components/form/form.tsx +++ b/src/components/form/form.tsx @@ -18,6 +18,8 @@ import { EuiCallOut } from '../call_out'; import { EuiI18n } from '../i18n'; import { CommonProps, ExclusiveUnion } from '../common'; +import { FormContext, FormContextValue } from './eui_form_context'; + export type EuiFormProps = CommonProps & ExclusiveUnion< { component: 'form' } & FormHTMLAttributes, @@ -33,6 +35,14 @@ export type EuiFormProps = CommonProps & * Where to display the callout with the list of errors */ invalidCallout?: 'above' | 'none'; + /** + * When set to `true`, all the rows/controls in this form will + * default to taking up 100% of the width of their continer. You + * can specify `fullWidth={false}` on individual rows/controls to + * disable this behavior for specific components. + * @default false + */ + fullWidth?: boolean; }; export const EuiForm = forwardRef( @@ -44,10 +54,18 @@ export const EuiForm = forwardRef( error, component = 'div', invalidCallout = 'above', + fullWidth, ...rest }, ref ) => { + const formContext = React.useMemo( + (): FormContextValue => ({ + defaultFullWidth: fullWidth ?? false, + }), + [fullWidth] + ); + const handleFocus = useCallback((node) => { node?.focus(); }, []); @@ -103,8 +121,10 @@ export const EuiForm = forwardRef( className={classes} {...(rest as HTMLAttributes)} > - {optionalErrorAlert} - {children} + + {optionalErrorAlert} + {children} + ); } diff --git a/src/components/form/form_control_layout/form_control_layout.test.tsx b/src/components/form/form_control_layout/form_control_layout.test.tsx index 716032c3a98..5e94e108aa1 100644 --- a/src/components/form/form_control_layout/form_control_layout.test.tsx +++ b/src/components/form/form_control_layout/form_control_layout.test.tsx @@ -11,6 +11,7 @@ import { render, mount } from 'enzyme'; import { findTestSubject, requiredProps } from '../../../test'; +import { EuiForm } from '../form'; import { EuiFormControlLayout, ICON_SIDES } from './form_control_layout'; jest.mock('../../', () => ({ @@ -209,4 +210,24 @@ describe('EuiFormControlLayout', () => { expect(component).toMatchSnapshot(); }); }); + + describe('inherits', () => { + test('fullWidth from ', () => { + const component = render( + + + + ); + + if ( + !component + .find('.euiFormControlLayout') + .hasClass('euiFormControlLayout--fullWidth') + ) { + throw new Error( + 'expected EuiFormControlLayout to inherit fullWidth from EuiForm' + ); + } + }); + }); }); 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 67ceeef2737..c585be791ac 100644 --- a/src/components/form/form_control_layout/form_control_layout.tsx +++ b/src/components/form/form_control_layout/form_control_layout.tsx @@ -21,6 +21,7 @@ import { } from './form_control_layout_icons'; import { CommonProps } from '../../common'; import { EuiFormLabel } from '../form_label'; +import { FormContext, FormContextValue } from '../eui_form_context'; export { ICON_SIDES } from './form_control_layout_icons'; @@ -42,6 +43,11 @@ export type EuiFormControlLayoutProps = CommonProps & children?: ReactNode; icon?: EuiFormControlLayoutIconsProps['icon']; clear?: EuiFormControlLayoutIconsProps['clear']; + /** + * Expand to fill 100% of the parent. + * Defaults to `fullWidth` prop of ``. + * @default false + */ fullWidth?: boolean; isLoading?: boolean; isDisabled?: boolean; @@ -60,12 +66,15 @@ export type EuiFormControlLayoutProps = CommonProps & }; export class EuiFormControlLayout extends Component { + static contextType = FormContext; + render() { + const { defaultFullWidth } = this.context as FormContextValue; const { children, icon, clear, - fullWidth, + fullWidth = defaultFullWidth, isLoading, isDisabled, compressed, 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 d077093a9ca..edf0e0a08fb 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 @@ -4,7 +4,6 @@ exports[`EuiFormRow behavior onBlur is called in child 1`] = ` { @@ -293,4 +294,22 @@ describe('EuiFormRow', () => { }); }); }); + + describe('inherits', () => { + test('fullWidth from ', () => { + const component = render( + + Label}> + + + + ); + + if (!component.find('.euiFormRow').hasClass('euiFormRow--fullWidth')) { + throw new Error( + 'expected EuiFormRow to inherit fullWidth from EuiForm' + ); + } + }); + }); }); diff --git a/src/components/form/form_row/form_row.tsx b/src/components/form/form_row/form_row.tsx index 84c85a76edd..e8b19680f9b 100644 --- a/src/components/form/form_row/form_row.tsx +++ b/src/components/form/form_row/form_row.tsx @@ -22,6 +22,7 @@ import { get } from '../../../services/objects'; import { EuiFormHelpText } from '../form_help_text'; import { EuiFormErrorText } from '../form_error_text'; import { EuiFormLabel } from '../form_label'; +import { FormContext, FormContextValue } from '../eui_form_context'; import { htmlIdGenerator } from '../../../services/accessibility'; @@ -56,6 +57,11 @@ type EuiFormRowCommonProps = CommonProps & { */ display?: EuiFormRowDisplayKeys; hasEmptyLabelSpace?: boolean; + /** + * Expand to fill 100% of the parent. + * Defaults to `fullWidth` prop of ``. + * @default false + */ fullWidth?: boolean; /** * IDs of additional elements that should be part of children's `aria-describedby` @@ -106,10 +112,11 @@ type LegendProps = { export type EuiFormRowProps = ExclusiveUnion; export class EuiFormRow extends Component { + static contextType = FormContext; + static defaultProps = { display: 'row', hasEmptyLabelSpace: false, - fullWidth: false, describedByIds: [], labelType: 'label', hasChildLabel: true, @@ -151,6 +158,8 @@ export class EuiFormRow extends Component { }; render() { + const { defaultFullWidth } = this.context as FormContextValue; + const { children, helpText, @@ -160,7 +169,7 @@ export class EuiFormRow extends Component { labelType, labelAppend, hasEmptyLabelSpace, - fullWidth, + fullWidth = defaultFullWidth, className, describedByIds, display, diff --git a/src/components/form/range/__snapshots__/range.test.tsx.snap b/src/components/form/range/__snapshots__/range.test.tsx.snap index 6232f31cf1d..712eb1d71bf 100644 --- a/src/components/form/range/__snapshots__/range.test.tsx.snap +++ b/src/components/form/range/__snapshots__/range.test.tsx.snap @@ -54,6 +54,32 @@ exports[`EuiRange allows value prop to accept empty string 1`] = ` `; +exports[`EuiRange inherits fullWidth from 1`] = ` +
+
+
+ +
+
+
+`; + exports[`EuiRange is rendered 1`] = `
{ expect(component).toMatchSnapshot(); }); }); + + describe('inherits', () => { + test('fullWidth from ', () => { + const component = render( + + {}} /> + + ); + + if ( + !component + .find('.euiRangeWrapper') + .hasClass('euiRangeWrapper--fullWidth') + ) { + throw new Error( + 'expected EuiDualRange to inherit fullWidth from EuiForm' + ); + } + }); + }); }); diff --git a/src/components/form/range/dual_range.tsx b/src/components/form/range/dual_range.tsx index 93b7a6fac7f..25dd8f3c94e 100644 --- a/src/components/form/range/dual_range.tsx +++ b/src/components/form/range/dual_range.tsx @@ -32,6 +32,7 @@ import { EuiRangeTick } from './range_ticks'; import { EuiRangeTrack } from './range_track'; import { EuiRangeWrapper } from './range_wrapper'; import { calculateThumbPosition } from './utils'; +import { FormContext, FormContextValue } from '../eui_form_context'; type ValueMember = number | string; @@ -56,6 +57,11 @@ export interface EuiDualRangeProps | React.KeyboardEvent | React.KeyboardEvent ) => void; + /** + * Expand to fill 100% of the parent. + * Defaults to `fullWidth` prop of ``. + * @default false + */ fullWidth?: boolean; isInvalid?: boolean; /** @@ -109,11 +115,12 @@ export interface EuiDualRangeProps } export class EuiDualRange extends Component { + static contextType = FormContext; + static defaultProps = { min: 0, max: 100, step: 1, - fullWidth: false, compressed: false, isLoading: false, showLabels: false, @@ -500,12 +507,13 @@ export class EuiDualRange extends Component { }; render() { + const { defaultFullWidth } = this.context as FormContextValue; const { className, css: customCss, compressed, disabled, - fullWidth, + fullWidth = defaultFullWidth, isLoading, readOnly, id: propsId, diff --git a/src/components/form/range/range.test.tsx b/src/components/form/range/range.test.tsx index 5f464629b1a..d8a3c6d1d62 100644 --- a/src/components/form/range/range.test.tsx +++ b/src/components/form/range/range.test.tsx @@ -10,6 +10,7 @@ import React from 'react'; import { render } from 'enzyme'; import { requiredProps } from '../../../test/required_props'; +import { EuiForm } from '../form'; import { EuiRange } from './range'; const props = { @@ -186,4 +187,16 @@ describe('EuiRange', () => { expect(component).toMatchSnapshot(); }); + + describe('inherits', () => { + test('fullWidth from ', () => { + const component = render( + + + + ); + + expect(component).toMatchSnapshot(); + }); + }); }); diff --git a/src/components/form/range/range.tsx b/src/components/form/range/range.tsx index b2e771955e8..81f74f33f93 100644 --- a/src/components/form/range/range.tsx +++ b/src/components/form/range/range.tsx @@ -23,12 +23,18 @@ import { EuiRangeTick } from './range_ticks'; import { EuiRangeTooltip } from './range_tooltip'; import { EuiRangeTrack } from './range_track'; import { EuiRangeWrapper } from './range_wrapper'; +import { FormContext, FormContextValue } from '../eui_form_context'; export interface EuiRangeProps extends CommonProps, Omit { compressed?: boolean; readOnly?: boolean; + /** + * Expand to fill 100% of the parent. + * Defaults to `fullWidth` prop of ``. + * @default false + */ fullWidth?: boolean; id?: string; /** @@ -87,11 +93,12 @@ export interface EuiRangeProps } export class EuiRange extends Component { + static contextType = FormContext; + static defaultProps = { min: 0, max: 100, step: 1, - fullWidth: false, compressed: false, isLoading: false, showLabels: false, @@ -163,11 +170,12 @@ export class EuiRange extends Component { }; render() { + const { defaultFullWidth } = this.context as FormContextValue; const { className, compressed, disabled, - fullWidth, + fullWidth = defaultFullWidth, isLoading, readOnly, id: propsId, diff --git a/src/components/form/range/range_wrapper.tsx b/src/components/form/range/range_wrapper.tsx index 59703447c75..ceb7bf9f261 100644 --- a/src/components/form/range/range_wrapper.tsx +++ b/src/components/form/range/range_wrapper.tsx @@ -9,16 +9,31 @@ import React, { HTMLAttributes, forwardRef } from 'react'; import classNames from 'classnames'; import { CommonProps } from '../../common'; +import { useFormContext } from '../eui_form_context'; export interface EuiRangeWrapperProps extends CommonProps, HTMLAttributes { + /** + * Expand to fill 100% of the parent. + * Defaults to `fullWidth` prop of ``. + * @default false + */ fullWidth?: boolean; compressed?: boolean; } export const EuiRangeWrapper = forwardRef( - ({ children, className, fullWidth, compressed, ...rest }, ref) => { + (props, ref) => { + const { defaultFullWidth } = useFormContext(); + const { + children, + className, + fullWidth = defaultFullWidth, + compressed, + ...rest + } = props; + const classes = classNames( 'euiRangeWrapper', { diff --git a/src/components/form/select/select.test.tsx b/src/components/form/select/select.test.tsx index 21eff6387bc..0598df5a916 100644 --- a/src/components/form/select/select.test.tsx +++ b/src/components/form/select/select.test.tsx @@ -11,6 +11,7 @@ import React from 'react'; import { render, mount } from 'enzyme'; import { requiredProps } from '../../../test/required_props'; +import { EuiForm } from '../form'; import { EuiSelect } from './select'; jest.mock('../form_control_layout', () => ({ @@ -153,4 +154,18 @@ describe('EuiSelect', () => { ).toBe(''); }); }); + + describe('inherits', () => { + test('fullWidth from ', () => { + const component = render( + + + + ); + + if (!component.find('.euiSelect').hasClass('euiSelect--fullWidth')) { + throw new Error('expected EuiSelect to inherit fullWidth from EuiForm'); + } + }); + }); }); diff --git a/src/components/form/select/select.tsx b/src/components/form/select/select.tsx index fff991f9d44..cd75d3fc560 100644 --- a/src/components/form/select/select.tsx +++ b/src/components/form/select/select.tsx @@ -19,6 +19,7 @@ import { EuiFormControlLayoutProps, } from '../form_control_layout'; import { EuiValidatableControl } from '../validatable_control'; +import { useFormContext } from '../eui_form_context'; import { getFormControlClassNameForIconCount } from '../form_control_layout/_num_icons'; export interface EuiSelectOption @@ -31,13 +32,22 @@ export type EuiSelectProps = Omit< 'value' > & CommonProps & { + /** + * @default [] + */ options?: EuiSelectOption[]; isInvalid?: boolean; + /** + * Expand to fill 100% of the parent. + * Defaults to `fullWidth` prop of ``. + * @default false + */ fullWidth?: boolean; isLoading?: boolean; /** * Simulates no selection by creating an empty, selected, hidden first option + * @default false */ hasNoInitialSelection?: boolean; inputRef?: Ref; @@ -45,6 +55,7 @@ export type EuiSelectProps = Omit< /** * when `true` creates a shorter height input + * @default false */ compressed?: boolean; @@ -60,25 +71,27 @@ export type EuiSelectProps = Omit< append?: EuiFormControlLayoutProps['append']; }; -export const EuiSelect: FunctionComponent = ({ - className, - options = [], - id, - name, - inputRef, - isInvalid, - fullWidth = false, - isLoading = false, - hasNoInitialSelection = false, - defaultValue, - compressed = false, - value: _value, - prepend, - append, - onMouseUp, - disabled, - ...rest -}) => { +export const EuiSelect: FunctionComponent = (props) => { + const { defaultFullWidth } = useFormContext(); + const { + className, + options = [], + id, + name, + inputRef, + isInvalid, + fullWidth = defaultFullWidth, + isLoading = false, + hasNoInitialSelection = false, + defaultValue, + compressed = false, + value: _value, + prepend, + append, + onMouseUp, + disabled, + ...rest + } = props; // if this is injecting an empty option for `hasNoInitialSelection` then // value needs to fallback to an empty string to interact properly with `defaultValue` const value = hasNoInitialSelection ? _value ?? '' : _value; diff --git a/src/components/form/super_select/super_select_control.test.tsx b/src/components/form/super_select/super_select_control.test.tsx index f3b85b4705f..d849e262b9c 100644 --- a/src/components/form/super_select/super_select_control.test.tsx +++ b/src/components/form/super_select/super_select_control.test.tsx @@ -10,6 +10,7 @@ import React from 'react'; import { render } from 'enzyme'; import { requiredProps } from '../../../test'; +import { EuiForm } from '../form'; import { EuiSuperSelectControl } from './super_select_control'; describe('EuiSuperSelectControl', () => { @@ -88,4 +89,24 @@ describe('EuiSuperSelectControl', () => { expect(component).toMatchSnapshot(); }); }); + + describe('inherits', () => { + test('fullWidth from ', () => { + const component = render( + + + + ); + + if ( + !component + .find('.euiSuperSelectControl') + .hasClass('euiSuperSelectControl--fullWidth') + ) { + throw new Error( + 'expected EuiSuperSelectControl to inherit fullWidth from EuiForm' + ); + } + }); + }); }); diff --git a/src/components/form/super_select/super_select_control.tsx b/src/components/form/super_select/super_select_control.tsx index 3eecc161f54..5f892b4f377 100644 --- a/src/components/form/super_select/super_select_control.tsx +++ b/src/components/form/super_select/super_select_control.tsx @@ -23,6 +23,7 @@ import { } from '../form_control_layout'; import { EuiI18n } from '../../i18n'; import { getFormControlClassNameForIconCount } from '../form_control_layout/_num_icons'; +import { useFormContext } from '../eui_form_context'; export interface EuiSuperSelectOption { value: T; @@ -35,9 +36,23 @@ export interface EuiSuperSelectOption { export interface EuiSuperSelectControlProps extends CommonProps, Omit, 'value'> { + /** + * @default false + */ compressed?: boolean; + /** + * Expand to fill 100% of the parent. + * Defaults to `fullWidth` prop of ``. + * @default false + */ fullWidth?: boolean; + /** + * @default false + */ isInvalid?: boolean; + /** + * @default false + */ isLoading?: boolean; readOnly?: boolean; @@ -66,24 +81,26 @@ export interface EuiSuperSelectControlProps export const EuiSuperSelectControl: ( props: EuiSuperSelectControlProps -) => ReturnType>> = ({ - className, - options = [], - id, - name, - fullWidth = false, - isLoading = false, - isInvalid = false, - readOnly, - defaultValue, - compressed = false, - value, - prepend, - append, - screenReaderId, - disabled, - ...rest -}) => { +) => ReturnType>> = (props) => { + const { defaultFullWidth } = useFormContext(); + const { + className, + options = [], + id, + name, + fullWidth = defaultFullWidth, + isLoading = false, + isInvalid = false, + readOnly, + defaultValue, + compressed = false, + value, + prepend, + append, + screenReaderId, + disabled, + ...rest + } = props; const numIconsClass = getFormControlClassNameForIconCount({ isInvalid, isLoading, diff --git a/src/components/form/text_area/text_area.test.tsx b/src/components/form/text_area/text_area.test.tsx index a01cdc6425e..e37622241a1 100644 --- a/src/components/form/text_area/text_area.test.tsx +++ b/src/components/form/text_area/text_area.test.tsx @@ -10,6 +10,7 @@ import React from 'react'; import { render } from 'enzyme'; import { requiredProps } from '../../../test/required_props'; +import { EuiForm } from '../form'; import { EuiTextArea } from './text_area'; describe('EuiTextArea', () => { @@ -18,4 +19,20 @@ describe('EuiTextArea', () => { expect(component).toMatchSnapshot(); }); + + describe('inherits', () => { + test('fullWidth from ', () => { + const component = render( + + + + ); + + if (!component.find('.euiTextArea').hasClass('euiTextArea--fullWidth')) { + throw new Error( + 'expected EuiTextArea to inherit fullWidth from EuiForm' + ); + } + }); + }); }); diff --git a/src/components/form/text_area/text_area.tsx b/src/components/form/text_area/text_area.tsx index 36123cb4acc..e9ee50fa4be 100644 --- a/src/components/form/text_area/text_area.tsx +++ b/src/components/form/text_area/text_area.tsx @@ -10,15 +10,22 @@ import React, { TextareaHTMLAttributes, Ref, FunctionComponent } from 'react'; import { CommonProps } from '../../common'; import classNames from 'classnames'; import { EuiValidatableControl } from '../validatable_control'; +import { useFormContext } from '../eui_form_context'; export type EuiTextAreaProps = TextareaHTMLAttributes & CommonProps & { isInvalid?: boolean; + /** + * Expand to fill 100% of the parent. + * Defaults to `fullWidth` prop of ``. + * @default false + */ fullWidth?: boolean; compressed?: boolean; /** * Which direction, if at all, should the textarea resize + * @default vertical */ resize?: keyof typeof resizeToClassNameMap; @@ -34,20 +41,23 @@ const resizeToClassNameMap = { export const RESIZE = Object.keys(resizeToClassNameMap); -export const EuiTextArea: FunctionComponent = ({ - children, - className, - compressed, - fullWidth = false, - id, - inputRef, - isInvalid, - name, - placeholder, - resize = 'vertical', - rows, - ...rest -}) => { +export const EuiTextArea: FunctionComponent = (props) => { + const { defaultFullWidth } = useFormContext(); + const { + children, + className, + compressed, + fullWidth = defaultFullWidth, + id, + inputRef, + isInvalid, + name, + placeholder, + resize = 'vertical', + rows, + ...rest + } = props; + const classes = classNames( 'euiTextArea', resizeToClassNameMap[resize], diff --git a/upcoming_changelogs/6229.md b/upcoming_changelogs/6229.md new file mode 100644 index 00000000000..b3383eddaed --- /dev/null +++ b/upcoming_changelogs/6229.md @@ -0,0 +1 @@ +- Added support for `fullWidth` prop on EuiForm, which will be the default for all rows/controls within