diff --git a/CHANGELOG.md b/CHANGELOG.md index a461a55d229..99dca07cb50 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ - Converted `EuiDualRange` to TypeScript ([#2732](https://github.com/elastic/eui/pull/2732)) - Converted `EuiRangeInput` to TypeScript ([#2732](https://github.com/elastic/eui/pull/2732)) - Added `bellSlash` glyph to `EuiIcon` ([#2714](https://github.com/elastic/eui/pull/2714)) +- Added `legend` prop to `EuiCheckboxGroup` and `EuiRadioGroup` to add `EuiFieldset` wrappers for title the groups ([#2739](https://github.com/elastic/eui/pull/2739)) + +**Bug fixes** + +- Changed `EuiRadio` and `EuiCheckbox` labels to be `inline-block` ([#2739](https://github.com/elastic/eui/pull/2739)) +- Fixed `EuiCheckboxGroup`'s `options` type to fully extend the `EuiCheckbox` type ([#2739](https://github.com/elastic/eui/pull/2739)) ## [`18.0.0`](https://github.com/elastic/eui/tree/v18.0.0) diff --git a/src-docs/src/views/card/card_checkable.js b/src-docs/src/views/card/card_checkable.js index c2e06416f0f..6befa90aace 100644 --- a/src-docs/src/views/card/card_checkable.js +++ b/src-docs/src/views/card/card_checkable.js @@ -5,6 +5,7 @@ import { EuiSpacer, EuiRadioGroup, EuiTitle, + EuiFormFieldset, } from '../../../../src/components'; import makeId from '../../../../src/components/form/form_row/make_id'; @@ -37,15 +38,14 @@ export default class extends Component { return ( -
- - - Checkable card radio group with legend - - - - - + + Checkable card radio group with legend + + ), + }}> this.setState({ radio: 'radio3' })} disabled /> -
+ diff --git a/src-docs/src/views/form_compressed/form_compressed.js b/src-docs/src/views/form_compressed/form_compressed.js index 373fd2552f7..512075c8388 100644 --- a/src-docs/src/views/form_compressed/form_compressed.js +++ b/src-docs/src/views/form_compressed/form_compressed.js @@ -10,6 +10,7 @@ import { EuiSelect, EuiSwitch, EuiPanel, + EuiSpacer, } from '../../../../src/components'; import makeId from '../../../../src/components/form/form_row/make_id'; @@ -135,14 +136,17 @@ export default class extends Component { /> - - - + + + ); } diff --git a/src-docs/src/views/form_controls/checkbox_group.js b/src-docs/src/views/form_controls/checkbox_group.js index b77915f6fd9..4aabaac80a7 100644 --- a/src-docs/src/views/form_controls/checkbox_group.js +++ b/src-docs/src/views/form_controls/checkbox_group.js @@ -1,10 +1,7 @@ -import React, { Component, Fragment } from 'react'; +import React, { Component } from 'react'; -import { - EuiCheckboxGroup, - EuiSpacer, - EuiTitle, -} from '../../../../src/components'; +import { EuiCheckboxGroup } from '../../../../src/components'; +import { DisplayToggles } from './display_toggles'; import makeId from '../../../../src/components/form/form_row/make_id'; @@ -18,39 +15,23 @@ export default class extends Component { { id: `${idPrefix}0`, label: 'Option one', + 'data-test-sub': 'dts_test', }, { id: `${idPrefix}1`, label: 'Option two is checked by default', + className: 'classNameTest', }, { id: `${idPrefix}2`, - label: 'Option three', + label: 'Option three is disabled', + disabled: true, }, ]; - this.checkboxesDisabled = this.checkboxes.map(checkbox => { - return { ...checkbox, id: `${checkbox.id}_disabled` }; - }); - - this.checkboxesIndividuallyDisabled = this.checkboxes.map(checkbox => { - const isIndividuallyDisabled = - checkbox.id.charAt(checkbox.id.length - 1) === '1'; - return { - ...checkbox, - id: `${checkbox.id}_individually_disabled`, - label: isIndividuallyDisabled - ? 'Option two is individually disabled' - : checkbox.label, - disabled: isIndividuallyDisabled, - }; - }); - this.state = { checkboxIdToSelectedMap: { [`${idPrefix}1`]: true, - [`${idPrefix}1_disabled`]: true, - [`${idPrefix}1_individually_disabled`]: true, }, }; } @@ -70,42 +51,18 @@ export default class extends Component { render() { return ( - + /* DisplayToggles wrapper for Docs only */ + - - - - -

Disabled

-
- - - - - - - - -

Individually disabled checkbox

-
- - - - -
+ ); } } 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 685883913e9..65784300e86 100644 --- a/src-docs/src/views/form_controls/form_controls_example.js +++ b/src-docs/src/views/form_controls/form_controls_example.js @@ -274,6 +274,20 @@ export const FormControlsExample = { EuiCheckboxGroup, }, demo: , + snippet: ` {}} +/>`, }, { title: 'Radio', @@ -308,6 +322,24 @@ export const FormControlsExample = { EuiRadioGroup, }, demo: , + snippet: ` {}} + name={groupName} + legend={{ + children: 'A legend', + }} +/>`, }, { title: 'Switch', @@ -360,7 +392,7 @@ export const FormControlsExample = { />

- EuiFieldset simply wraps its children in a{' '} + EuiFormFieldset simply wraps its children in a{' '} <fieldset> with the option to add a{' '} <legend> via the legend{' '} object prop. diff --git a/src-docs/src/views/form_controls/radio_group.js b/src-docs/src/views/form_controls/radio_group.js index b070fbd65ea..b22e8e88728 100644 --- a/src-docs/src/views/form_controls/radio_group.js +++ b/src-docs/src/views/form_controls/radio_group.js @@ -1,8 +1,9 @@ -import React, { Component, Fragment } from 'react'; +import React, { Component } from 'react'; -import { EuiRadioGroup, EuiSpacer, EuiTitle } from '../../../../src/components'; +import { EuiRadioGroup } from '../../../../src/components'; import makeId from '../../../../src/components/form/form_row/make_id'; +import { DisplayToggles } from './display_toggles'; export default class extends Component { constructor(props) { @@ -21,7 +22,7 @@ export default class extends Component { }, { id: `${idPrefix}2`, - label: 'Option three', + label: 'Option three is disabled', disabled: true, }, ]; @@ -39,29 +40,22 @@ export default class extends Component { render() { return ( - + /* DisplayToggles wrapper for Docs only */ + This is a legend for a radio group, + }} /> - - - - -

Disabled

- - - - - -
+ ); } } diff --git a/src-docs/src/views/form_layouts/form_rows.js b/src-docs/src/views/form_layouts/form_rows.js index be585ff3326..5cb3a437aae 100644 --- a/src-docs/src/views/form_layouts/form_rows.js +++ b/src-docs/src/views/form_layouts/form_rows.js @@ -128,15 +128,17 @@ export default class extends Component { /> - - - + + + diff --git a/src-docs/src/views/range/states.js b/src-docs/src/views/range/states.js index 39b20e48929..08833069d71 100644 --- a/src-docs/src/views/range/states.js +++ b/src-docs/src/views/range/states.js @@ -60,7 +60,7 @@ export default class extends Component { - + `; +exports[`EuiCheckboxGroup (mocked checkbox) legend is rendered 1`] = ` +
+ + A legend + +
+`; + exports[`EuiCheckboxGroup (mocked checkbox) options are rendered 1`] = `
{ {}} /> ); @@ -46,7 +45,6 @@ describe('EuiCheckboxGroup (mocked checkbox)', () => { '1': true, '2': false, }} - onChange={() => {}} /> ); @@ -62,7 +60,6 @@ describe('EuiCheckboxGroup (mocked checkbox)', () => { '1': true, '2': false, }} - onChange={() => {}} disabled /> ); @@ -82,7 +79,19 @@ describe('EuiCheckboxGroup (mocked checkbox)', () => { '1': true, '2': false, }} - onChange={() => {}} + /> + ); + + expect(component).toMatchSnapshot(); + }); + + test('legend is rendered', () => { + const component = render( + ); diff --git a/src/components/form/checkbox/checkbox_group.tsx b/src/components/form/checkbox/checkbox_group.tsx index 0b503c4d5be..7911935bccd 100644 --- a/src/components/form/checkbox/checkbox_group.tsx +++ b/src/components/form/checkbox/checkbox_group.tsx @@ -1,19 +1,36 @@ -import React, { ReactNode, FunctionComponent } from 'react'; +import React, { FunctionComponent, HTMLAttributes } from 'react'; +import classNames from 'classnames'; -import { EuiCheckbox } from './checkbox'; -import { CommonProps } from '../../common'; +import { CommonProps, ExclusiveUnion } from '../../common'; -export interface EuiCheckboxGroupOption { +import { + EuiFormFieldsetProps, + EuiFormLegendProps, + EuiFormFieldset, +} from '../form_fieldset'; +import { EuiCheckbox, EuiCheckboxProps } from './checkbox'; + +export interface EuiCheckboxGroupOption + extends Omit { id: string; - label?: ReactNode; - disabled?: boolean; } export interface EuiCheckboxGroupIdToSelectedMap { [id: string]: boolean; } -export interface EuiCheckboxGroupProps extends CommonProps { +// Must omit inherit `onChange` properties or else TS complaines when applying to the EuiRadio +type AsDivProps = Omit, 'onChange'>; +type WithLegendProps = Omit & { + /** + * If the individual labels for each radio do not provide a sufficient description, add a legend. + * Wraps the group in a `EuiFormFieldset` which adds an `EuiLegend` for titling the whole group. + * Accepts an `EuiFormLegendProps` shape. + */ + legend?: EuiFormLegendProps; +}; + +export type EuiCheckboxGroupProps = CommonProps & { options: EuiCheckboxGroupOption[]; idToSelectedMap: EuiCheckboxGroupIdToSelectedMap; onChange: (optionId: string) => void; @@ -23,7 +40,7 @@ export interface EuiCheckboxGroupProps extends CommonProps { */ compressed?: boolean; disabled?: boolean; -} +} & ExclusiveUnion; export const EuiCheckboxGroup: FunctionComponent = ({ options = [], @@ -32,22 +49,45 @@ export const EuiCheckboxGroup: FunctionComponent = ({ className, disabled, compressed, + legend, ...rest -}) => ( -
- {options.map((option, index) => { - return ( - - ); - })} -
-); +}) => { + const checkboxes = options.map((option, index) => { + const { + disabled: isOptionDisabled, + className: optionClass, + ...optionRest + } = option; + return ( + + ); + }); + + if (!!legend) { + // Be sure to pass down the compressed option to the legend + legend.compressed = compressed; + + return ( + + {checkboxes} + + ); + } + + return ( +
}> + {checkboxes} +
+ ); +}; diff --git a/src/components/form/radio/__snapshots__/radio_group.test.tsx.snap b/src/components/form/radio/__snapshots__/radio_group.test.tsx.snap index 1cfb7a45ec3..d6f708c969a 100644 --- a/src/components/form/radio/__snapshots__/radio_group.test.tsx.snap +++ b/src/components/form/radio/__snapshots__/radio_group.test.tsx.snap @@ -50,6 +50,52 @@ exports[`EuiRadioGroup props idSelected is rendered 1`] = `
`; +exports[`EuiRadioGroup props legend is rendered 1`] = ` +
+ + A legend + +
+ +
+ +
+
+ +
+ +
+
+`; + exports[`EuiRadioGroup props name is propagated to radios 1`] = `
{ expect(component).toMatchSnapshot(); }); + + test('legend is rendered', () => { + const component = render( + {}} + legend={{ + children: 'A legend', + }} + /> + ); + + expect(component).toMatchSnapshot(); + }); }); describe('callbacks', () => { diff --git a/src/components/form/radio/radio_group.tsx b/src/components/form/radio/radio_group.tsx index f6ef04aa202..2db01015166 100644 --- a/src/components/form/radio/radio_group.tsx +++ b/src/components/form/radio/radio_group.tsx @@ -1,28 +1,45 @@ import React, { FunctionComponent, HTMLAttributes } from 'react'; -import { CommonProps } from '../../common'; +import classNames from 'classnames'; -import { EuiRadio, RadioProps } from './radio'; +import { CommonProps, ExclusiveUnion } from '../../common'; + +import { + EuiFormFieldsetProps, + EuiFormLegendProps, + EuiFormFieldset, +} from '../form_fieldset'; +import { EuiRadio, EuiRadioProps } from './radio'; export interface EuiRadioGroupOption - extends Omit { + extends Omit { id: string; } export type EuiRadioGroupChangeCallback = (id: string, value?: string) => void; -export type EuiRadioGroupProps = CommonProps & - Omit, 'onChange'> & { - disabled?: boolean; - /** - * Tightens up the spacing between radio rows and sends down the - * compressed prop to the radio itself - */ - compressed?: boolean; - name?: string; - options: EuiRadioGroupOption[]; - idSelected?: string; - onChange: EuiRadioGroupChangeCallback; - }; +// Must omit inherit `onChange` properties or else TS complaines when applying to the EuiRadio +type AsDivProps = Omit, 'onChange'>; +type WithLegendProps = Omit & { + /** + * If the individual labels for each radio do not provide a sufficient description, add a legend. + * Wraps the group in a `EuiFormFieldset` which adds an `EuiLegend` for titling the whole group. + * Accepts an `EuiFormLegendProps` shape. + */ + legend?: EuiFormLegendProps; +}; + +export type EuiRadioGroupProps = CommonProps & { + disabled?: boolean; + /** + * Tightens up the spacing between radio rows and sends down the + * compressed prop to the radio itself + */ + compressed?: boolean; + name?: string; + options: EuiRadioGroupOption[]; + idSelected?: string; + onChange: EuiRadioGroupChangeCallback; +} & ExclusiveUnion; export const EuiRadioGroup: FunctionComponent = ({ options = [], @@ -32,23 +49,46 @@ export const EuiRadioGroup: FunctionComponent = ({ className, disabled, compressed, + legend, ...rest -}) => ( -
- {options.map((option, index) => { - const { disabled: isOptionDisabled, ...optionRest } = option; - return ( - - ); - })} -
-); +}) => { + const radios = options.map((option, index) => { + const { + disabled: isOptionDisabled, + className: optionClass, + ...optionRest + } = option; + return ( + + ); + }); + + if (!!legend) { + // Be sure to pass down the compressed option to the legend + legend.compressed = compressed; + + return ( + + {radios} + + ); + } + + return ( +
}> + {radios} +
+ ); +};