diff --git a/CHANGELOG.md b/CHANGELOG.md
index de2eb5ed089..ce69ef48da0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,14 @@
## [`master`](https://github.com/elastic/eui/tree/master)
+- Added `minWidth` prop to `EuiButton` ([4056](https://github.com/elastic/eui/pull/4056))
+- Added `isSelected` prop to easily turn `EuiButton`, `EuiButtonEmpty`, and `EuiButtonIcon` into toggle buttons ([4056](https://github.com/elastic/eui/pull/4056))
+- Updated `EuiButtonGroup` props and render for better accessibility ([4056](https://github.com/elastic/eui/pull/4056))
+
+**Breaking changes**
+
+- Removed `EuiToggle` and `EuiButtonToggle` in favor of `aria-pressed` ([4056](https://github.com/elastic/eui/pull/4056))
+- Updated `legend` and `idSelected` props of `EuiButtonGroup` to be required ([4056](https://github.com/elastic/eui/pull/4056))
+
**Theme: Amsterdam**
- Tightened `line-height` for some `EuiTitle` sizes ([4133](https://github.com/elastic/eui/pull/4133))
diff --git a/packages/eslint-plugin/rules/href_or_on_click.js b/packages/eslint-plugin/rules/href_or_on_click.js
index e0533fa9455..e0960ae1f65 100644
--- a/packages/eslint-plugin/rules/href_or_on_click.js
+++ b/packages/eslint-plugin/rules/href_or_on_click.js
@@ -1,4 +1,4 @@
-const componentNames = ['EuiButton', 'EuiButtonEmpty', 'EuiLink'];
+const componentNames = ['EuiButton', 'EuiButtonEmpty', 'EuiLink', 'EuiBadge'];
module.exports = {
meta: {
@@ -24,9 +24,7 @@ module.exports = {
if (hasHref && hasOnClick) {
context.report(
node,
- `<${
- node.name.name
- }> accepts either \`href\` or \`onClick\`, not both.`
+ `<${node.name.name}> accepts either \`href\` or \`onClick\`, not both.`
);
}
},
diff --git a/src-docs/src/routes.js b/src-docs/src/routes.js
index 2e361642dd5..0261f3558a3 100644
--- a/src-docs/src/routes.js
+++ b/src-docs/src/routes.js
@@ -204,8 +204,6 @@ import { ToastExample } from './views/toast/toast_example';
import { ToolTipExample } from './views/tool_tip/tool_tip_example';
-import { ToggleExample } from './views/toggle/toggle_example';
-
import { TourExample } from './views/tour/tour_example';
import { WindowEventExample } from './views/window_event/window_event_example';
@@ -468,7 +466,6 @@ const navigation = [
ResizeObserverExample,
ResponsiveExample,
TextDiffExample,
- ToggleExample,
WindowEventExample,
].map((example) => createExample(example)),
},
diff --git a/src-docs/src/views/button/button_example.js b/src-docs/src/views/button/button_example.js
index e557bd1db61..b8a754a86ba 100644
--- a/src-docs/src/views/button/button_example.js
+++ b/src-docs/src/views/button/button_example.js
@@ -1,9 +1,7 @@
import React from 'react';
-
import { Link } from 'react-router-dom';
import { renderToHtml } from '../../services';
-
import { GuideSectionTypes } from '../../components';
import {
@@ -12,12 +10,14 @@ import {
EuiButtonIcon,
EuiCode,
EuiButtonGroup,
- EuiButtonToggle,
EuiCallOut,
EuiText,
} from '../../../../src/components';
+
+import { EuiButtonGroupOptionProps } from '!!prop-loader!../../../../src/components/button/button_group/button_group';
+
import Guidelines from './guidelines';
-import buttonConfig from './playground';
+import Playground from './playground';
import Button from './button';
const buttonSource = require('!!raw-loader!./button');
@@ -82,30 +82,60 @@ const buttonLoadingSnippet = `
import ButtonToggle from './button_toggle';
const buttonToggleSource = require('!!raw-loader!./button_toggle');
const buttonToggleHtml = renderToHtml(ButtonToggle);
-const buttonToggleSnippet = `
+ {toggleOn ? onLabel : offLabel}
+
+`,
+ ``;
+ fill={toggleOn}
+ onClick={onToggleChange}
+ >
+
+`,
+ `
+
+`,
+];
import ButtonGroup from './button_group';
const buttonGroupSource = require('!!raw-loader!./button_group');
const buttonGroupHtml = renderToHtml(ButtonGroup);
const buttonGroupSnippet = [
` {}}
/>`,
` {}}
/>`,
];
@@ -292,26 +322,43 @@ export const ButtonExample = {
},
],
text: (
-
+ <>
- This is a specialized component that combines{' '}
- EuiButton and EuiToggle to create
- a button with an on/off state. You can pass all the same parameters
- to it as you can to EuiButton. The main difference
- is that, it does not accept any children, but a{' '}
- label prop instead. This is for the handling of
- accessibility with the EuiToggle.
+ You can create a toggle style button with any button type like the
+ standard EuiButton, EuiButtonEmpty
+ , or EuiButtonIcon. Use state management to handle
+ the visual differences for on and off. Though there are two{' '}
+ exclusive situations to consider.
-
- The EuiButtonToggle does not have any inherit
- visual state differences. These you must apply in your
- implementation.
-
-
+
+
+ If your button changes its readable text, via
+ children or aria-label, then there is no
+ additional accessibility concern.
+
+
+ If your button only changes the visual{' '}
+ appearance, you must add aria-pressed passing a
+ boolean for the on and off states. All EUI button types provide a
+ helper prop for this called isSelected.
+
+
+
+ Do not add aria-pressed or{' '}
+ isSelected if you also change the readable
+ text.
+
+ }
+ />
+ >
),
demo: ,
snippet: buttonToggleSnippet,
- props: { EuiButtonToggle },
+ props: { EuiButton, EuiButtonIcon },
},
{
title: 'Button groups',
@@ -328,19 +375,11 @@ export const ButtonExample = {
text: (
- EuiButtonGroups are handled similarly to the way
- checkbox and radio groups are handled but made to look like buttons.
- They group multiple EuiButtonToggles and utilize
- the type="single" or{' '}
+ EuiButtonGroups utilize the{' '}
+ type="single" or{' '}
"multi" prop to determine
- whether multiple or only single selections are allowed per group.
-
-
- Stylistically, all button groups are the size of small buttons, do
- not stretch to fill the container, and typically should only be{' '}
- color="text" (default) or{' '}
- "primary". If you're
- just displaying a group of icons, add the prop{' '}
+ whether multiple or only single selections are allowed per group. If
+ you're just displaying a group of icons, add the prop{' '}
isIconOnly.
- The EuiToggle component is a very simplified
- utility for creating toggle-able elements. There is only an on/off
- (checked/unchecked) state. All this creates is a visibly hidden
- input (checkbox or radio) overtop of the children provided.
-
-
- By default, the children will be wrapped in a block element. To
- change the display you can simply use one of the{' '}
- utility classes{' '}
- like .eui-displayInlineBlock.
-
-
- This utility is just a helper component and comes with no
- inherit styles including no :hover or{' '}
- :focus states. If you use this utility
- directly, be sure to add these states. Otherwise, you may just
- want to utilize the{' '}
- EuiButtonToggle component.
-
- }
- />
-
- ),
- components: { EuiToggle },
- snippet: toggleSnippet,
- demo: ,
- props: { EuiToggle },
- },
- ],
- playground: toggleConfig,
-};
diff --git a/src/components/accessibility/__snapshots__/skip_link.test.tsx.snap b/src/components/accessibility/__snapshots__/skip_link.test.tsx.snap
index 5a12b3e9c5c..2f03a9a93e3 100644
--- a/src/components/accessibility/__snapshots__/skip_link.test.tsx.snap
+++ b/src/components/accessibility/__snapshots__/skip_link.test.tsx.snap
@@ -3,7 +3,7 @@
exports[`EuiSkipLink is rendered 1`] = `
@@ -38,7 +38,7 @@ exports[`EuiSkipLink props onClick is rendered 1`] = `
exports[`EuiSkipLink props position absolute is rendered 1`] = `
@@ -54,7 +54,7 @@ exports[`EuiSkipLink props position absolute is rendered 1`] = `
exports[`EuiSkipLink props position fixed is rendered 1`] = `
@@ -87,7 +87,7 @@ exports[`EuiSkipLink props position static is rendered 1`] = `
exports[`EuiSkipLink props tabIndex is rendered 1`] = `
`;
+exports[`EuiButton props isSelected is rendered as false 1`] = `
+
+`;
+
+exports[`EuiButton props isSelected is rendered as true 1`] = `
+
+`;
+
+exports[`EuiButton props minWidth is rendered 1`] = `
+
+`;
+
exports[`EuiButton props size m is rendered 1`] = `
`;
+exports[`EuiButtonEmpty props isSelected is rendered as false 1`] = `
+
+`;
+
+exports[`EuiButtonEmpty props isSelected is rendered as true 1`] = `
+
+`;
+
exports[`EuiButtonEmpty props size l is rendered 1`] = `
diff --git a/src/components/button/button_group/__snapshots__/button_group.test.tsx.snap b/src/components/button/button_group/__snapshots__/button_group.test.tsx.snap
index c895ae057d4..0db6d7464d8 100644
--- a/src/components/button/button_group/__snapshots__/button_group.test.tsx.snap
+++ b/src/components/button/button_group/__snapshots__/button_group.test.tsx.snap
@@ -1,1663 +1,2270 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`EuiButtonGroup is rendered 1`] = `
+exports[`EuiButtonGroup button props buttonSize compressed is rendered for multi 1`] = `
+`;
+
+exports[`EuiButtonGroup button props buttonSize compressed is rendered for single 1`] = `
+
+`;
+
+exports[`EuiButtonGroup button props buttonSize m is rendered for multi 1`] = `
+
+`;
+
+exports[`EuiButtonGroup button props buttonSize m is rendered for single 1`] = `
+
+`;
+
+exports[`EuiButtonGroup button props buttonSize s is rendered for multi 1`] = `
+
+`;
+
+exports[`EuiButtonGroup button props buttonSize s is rendered for single 1`] = `
+
+`;
+
+exports[`EuiButtonGroup button props color danger is rendered for multi 1`] = `
+
+`;
+
+exports[`EuiButtonGroup button props color danger is rendered for single 1`] = `
+
+`;
+
+exports[`EuiButtonGroup button props color ghost is rendered for multi 1`] = `
+
`;
-exports[`EuiButtonGroup props buttonSize compressed is rendered 1`] = `
+exports[`EuiButtonGroup button props color ghost is rendered for single 1`] = `
`;
-exports[`EuiButtonGroup props buttonSize m is rendered 1`] = `
+exports[`EuiButtonGroup button props color primary is rendered for multi 1`] = `
`;
-exports[`EuiButtonGroup props buttonSize s is rendered 1`] = `
+exports[`EuiButtonGroup button props color primary is rendered for single 1`] = `
`;
-exports[`EuiButtonGroup props color danger is rendered 1`] = `
+exports[`EuiButtonGroup button props color secondary is rendered for multi 1`] = `
+`;
+
+exports[`EuiButtonGroup button props color secondary is rendered for single 1`] = `
+
`;
-exports[`EuiButtonGroup props color ghost is rendered 1`] = `
+exports[`EuiButtonGroup button props color text is rendered for multi 1`] = `
+`;
+
+exports[`EuiButtonGroup button props color text is rendered for single 1`] = `
+
+`;
+
+exports[`EuiButtonGroup button props color warning is rendered for multi 1`] = `
+
`;
-exports[`EuiButtonGroup props color primary is rendered 1`] = `
+exports[`EuiButtonGroup button props color warning is rendered for single 1`] = `
-`;
-
-exports[`EuiButtonGroup props color secondary is rendered 1`] = `
-
`;
-exports[`EuiButtonGroup props color text is rendered 1`] = `
+exports[`EuiButtonGroup button props isDisabled is rendered for multi 1`] = `
-`;
-
-exports[`EuiButtonGroup props color warning is rendered 1`] = `
-
`;
-exports[`EuiButtonGroup props idSelected is rendered 1`] = `
+exports[`EuiButtonGroup button props isDisabled is rendered for single 1`] = `
-`;
-
-exports[`EuiButtonGroup props isDisabled is rendered 1`] = `
-
`;
-exports[`EuiButtonGroup props isFullWidth is rendered 1`] = `
+exports[`EuiButtonGroup button props isFullWidth is rendered for multi 1`] = `
`;
-exports[`EuiButtonGroup props isIconOnly is rendered 1`] = `
+exports[`EuiButtonGroup button props isFullWidth is rendered for single 1`] = `
`;
-exports[`EuiButtonGroup props legend is rendered 1`] = `
+exports[`EuiButtonGroup button props isIconOnly is rendered for multi 1`] = `
`;
-exports[`EuiButtonGroup props name is rendered 1`] = `
+exports[`EuiButtonGroup button props isIconOnly is rendered for single 1`] = `
`;
-exports[`EuiButtonGroup props options are rendered 1`] = `
+exports[`EuiButtonGroup button props selection idSelected is rendered for single 1`] = `
`;
-exports[`EuiButtonGroup props options can pass down data-test-subj 1`] = `
+exports[`EuiButtonGroup button props selection idToSelectedMap is rendered for multi 1`] = `
`;
-exports[`EuiButtonGroup props type of multi idToSelectedMap is rendered 1`] = `
+exports[`EuiButtonGroup type multi is rendered 1`] = `
`;
-exports[`EuiButtonGroup props type of multi is rendered 1`] = `
+exports[`EuiButtonGroup type single is rendered 1`] = `
`;
diff --git a/src/components/button/button_group/_button_group.scss b/src/components/button/button_group/_button_group.scss
index 6251cf8fec0..1047b94e9ee 100644
--- a/src/components/button/button_group/_button_group.scss
+++ b/src/components/button/button_group/_button_group.scss
@@ -1,138 +1,55 @@
.euiButtonGroup {
- max-width: 100%;
- display: flex;
-}
-
-.euiButtonGroup__fieldset {
display: inline-block;
max-width: 100%;
-
- &--fullWidth {
- display: block;
- }
}
.euiButtonGroup--fullWidth {
- .euiButtonGroup__toggle {
- flex: 1;
- }
-}
-
-.euiButtonGroup__toggle {
- margin-left: -1px;
- z-index: 1;
-
- // DO NOT Transform
- // sass-lint:disable-block no-important
- transition: none !important;
- transform: none !important;
- animation: none !important;
-
- &[class*='checked'] {
- z-index: 2; // Raise it above the simply bordered versions for crisper lines
+ display: block;
- // add a slight divider if two selected items are next to each other
- + [class*='checked'] {
- box-shadow: -1px 0 0 transparentize($euiColorEmptyShade, .9);
- }
- }
-
- .euiButtonGroup__button {
- border-radius: 0;
+ .euiButtonGroup__buttons {
width: 100%;
- // DO NOT Transform
- // sass-lint:disable-block no-important
- transition: none !important;
- transform: none !important;
- animation: none !important;
-
- // always the same border color unless it's selected
- &:not([class*='fill']) {
- border-color: $euiButtonToggleBorderColor;
- }
-
- // don't colorize the shadows
- &:enabled {
- @include euiSlightShadow;
+ .euiButtonGroupButton {
+ flex: 1;
}
}
+}
- &:first-child {
- margin-left: 0;
-
- .euiButtonGroup__button {
- border-top-left-radius: $euiBorderRadius;
- border-bottom-left-radius: $euiBorderRadius;
- }
- }
+.euiButtonGroup__buttons {
+ @include euiSlightShadow;
+ border-radius: $euiBorderRadius + 1px; // Simply for the box-shadow
+ max-width: 100%;
+ display: flex;
+ overflow: hidden;
+ transition: all $euiAnimSpeedNormal ease-in-out;
- &:last-child .euiButtonGroup__button {
- border-top-right-radius: $euiBorderRadius;
- border-bottom-right-radius: $euiBorderRadius;
+ &:hover,
+ &:active,
+ &:focus-within {
+ @include euiSlightShadowHover;
}
-
}
-@include euiBreakpoint('xs', 's') {
- .euiButtonGroup__fieldset {
- display: block;
- }
-
- .euiButtonGroup__toggle {
- flex: 1;
- min-width: 0;
-
- .euiButtonGroup__button {
- min-width: 0;
- }
+.euiButtonGroup--isDisabled .euiButtonGroup__buttons {
+ &:hover,
+ &:active,
+ &:focus-within {
+ box-shadow: none !important; // sass-lint:disable-line no-important
}
}
.euiButtonGroup--compressed {
- border: 1px solid $euiFormBorderColor;
- border-radius: $euiFormControlCompressedBorderRadius;
- background-color: $euiFormBackgroundColor;
-
- .euiButtonGroup__button {
- height: $euiFormControlCompressedHeight - 2px;
- // sass-lint:disable-block no-important
- box-shadow: none !important;
- font-size: $euiFontSizeS;
- min-width: 0;
- border: none;
- border-radius: $euiBorderRadius;
- // Offset the background color from the border by 2px
- // by clipping background to before the padding starts
- padding: 2px;
- background-clip: content-box;
-
- &:not(.euiButtonGroup__button--selected):not(:disabled) {
- color: $euiColorDarkShade;
- }
-
- .euiButton__content {
- padding-left: $euiSizeS;
- padding-right: $euiSizeS;
- }
- }
-
- .euiButtonGroup__toggle {
- flex: 1;
- min-width: 0;
- }
-
- .euiButtonToggle__input:enabled:hover + .euiButtonGroup__button,
- .euiButtonToggle__input:enabled:focus + .euiButtonGroup__button {
- background-color: transparentize($euiFormInputGroupLabelBackground, .5);
- }
-
- .euiButtonToggle__input:enabled:focus + .euiButtonGroup__button {
- outline: 2px solid $euiFocusRingColor;
- }
-
- .euiButtonGroup__button--selected {
- font-weight: $euiFontWeightSemiBold;
- background-color: $euiFormInputGroupLabelBackground;
+ .euiButtonGroup__buttons {
+ box-shadow: none !important; // sass-lint:disable-line no-important
+ border-radius: $euiFormControlCompressedBorderRadius;
+ background-color: $euiFormBackgroundColor;
+ height: $euiFormControlCompressedHeight;
+ border: 1px solid $euiFormBorderColor;
+ overflow: visible;
+ }
+
+ .euiButtonGroupButton {
+ // Add 2 to the border radius to account for the background-clip
+ border-radius: $euiFormControlCompressedBorderRadius + 2;
}
}
diff --git a/src/components/button/button_group/_button_group_button.scss b/src/components/button/button_group/_button_group_button.scss
new file mode 100644
index 00000000000..cd6433f7752
--- /dev/null
+++ b/src/components/button/button_group/_button_group_button.scss
@@ -0,0 +1,215 @@
+.euiButtonGroupButton {
+ @include euiButtonBase;
+ @include euiFont;
+ @include euiFontSize;
+
+ // sass-lint:disable-block indentation
+ transition: background-color $euiAnimSpeedNormal ease-in-out,
+ border-color $euiAnimSpeedNormal ease-in-out,
+ color $euiAnimSpeedNormal ease-in-out;
+
+ // Allow button to shrink and truncate
+ min-width: 0;
+ flex-shrink: 1;
+ flex-grow: 0;
+
+ .euiButton__content {
+ padding: 0 $euiSizeM;
+ }
+
+ &-isIconOnly .euiButton__content {
+ padding: 0 $euiSizeS;
+ }
+
+ .euiButton__text {
+ text-overflow: ellipsis;
+ overflow: hidden;
+ }
+
+ &.euiButtonGroupButton--small {
+ height: $euiButtonHeightSmall;
+ line-height: $euiButtonHeightSmall; // prevents descenders from getting cut off
+ }
+
+ &:not([class*='isDisabled']) {
+ &:hover,
+ &:focus,
+ &:focus-within {
+ background-color: transparentize($euiColorPrimary, .9);
+ text-decoration: underline;
+ }
+ }
+
+ &.euiButtonGroupButton-isDisabled {
+ @include euiButtonContentDisabled;
+ color: $euiButtonColorDisabledText;
+
+ &.euiButtonGroupButton-isSelected {
+ // Only increase the contrast of background color to text to 2.0 for disabled
+ color: makeHighContrastColor($euiButtonColorDisabled, $euiButtonColorDisabled, 2);
+ }
+ }
+}
+
+.euiButtonGroupButton__textShift {
+ @include euiTextShift;
+}
+
+/**
+ * Medium and Small sizing (regular button style)
+ */
+
+// sass-lint:disable nesting-depth
+.euiButtonGroup--medium,
+.euiButtonGroup--small {
+ .euiButtonGroupButton {
+ border: $euiBorderThin;
+
+ &:not(:first-child) {
+ margin-left: -1px;
+ }
+
+ &:first-child {
+ border-radius: $euiBorderRadius 0 0 $euiBorderRadius;
+ }
+
+ &:last-child {
+ border-radius: 0 $euiBorderRadius $euiBorderRadius 0;
+ }
+ }
+
+ .euiButtonGroupButton-isDisabled {
+ &.euiButtonGroupButton-isSelected {
+ background-color: $euiButtonColorDisabled;
+ border-color: $euiButtonColorDisabled;
+
+ &:hover,
+ &:focus,
+ &:focus-within {
+ background-color: $euiButtonColorDisabled;
+ border-color: $euiButtonColorDisabled;
+ }
+ }
+ }
+
+ @each $name, $color in $euiButtonTypes {
+ .euiButtonGroupButton--#{$name}:not([class*='isDisabled']) {
+ @if ($name == 'ghost') {
+ // Ghost is unique and ALWAYS sits against a dark background.
+ color: $color;
+ } @else if ($name == 'text') {
+ // The default color is lighter than the normal text color, make the it the text color
+ color: $euiTextColor;
+ } @else {
+ // Other colors need to check their contrast against the page background color.
+ color: makeHighContrastColor($color, $euiPageBackgroundColor);
+ }
+
+ &.euiButtonGroupButton-isSelected {
+ background-color: $color;
+ border-color: $color;
+
+ // The function makes that hexes safe for theming
+ $fillTextColor: chooseLightOrDarkText($color, $euiColorGhost, $euiColorInk);
+
+ color: $fillTextColor;
+
+ &:hover,
+ &:focus,
+ &:focus-within {
+ background-color: darken($color, 5%);
+ border-color: darken($color, 5%);
+ }
+ }
+
+ &:hover,
+ &:focus,
+ &:focus-within {
+ background-color: transparentize($color, .9);
+ }
+ }
+ }
+
+ // Fix ghost/disabled look specifically
+ .euiButtonGroupButton.euiButtonGroupButton-isDisabled.euiButtonGroupButton--ghost {
+ &,
+ &:hover,
+ &:focus,
+ &:focus-within {
+ color: $euiButtonColorGhostDisabled;
+ }
+
+ .euiButtonGroup--isDisabled & {
+ border-color: $euiButtonColorGhostDisabled;
+ }
+
+ &.euiButtonGroupButton-isSelected {
+ background-color: $euiButtonColorGhostDisabled;
+ color: makeHighContrastColor($euiButtonColorGhostDisabled, $euiButtonColorGhostDisabled, 2);
+ }
+ }
+
+ .euiButtonGroupButton-isSelected {
+ z-index: 0;
+ }
+
+ .euiButtonGroupButton-isSelected + .euiButtonGroupButton-isSelected {
+ box-shadow: -1px 0 0 transparentize($euiColorEmptyShade, .9);
+ }
+}
+
+/**
+ * Compressed (form style)
+ */
+
+.euiButtonGroup--compressed {
+ .euiButtonGroupButton {
+ height: $euiFormControlCompressedHeight - 2px;
+ line-height: $euiFormControlCompressedHeight - 2px; // prevents descenders from getting cut off
+ font-size: $euiFontSizeS;
+ border-radius: $euiBorderRadius;
+ // Offset the background color from the border by 2px
+ // by clipping background to before the padding starts
+ padding: 2px;
+ background-clip: content-box;
+
+ .euiButton__content {
+ padding-left: $euiSizeS;
+ padding-right: $euiSizeS;
+ }
+
+ &.euiButtonGroupButton-isSelected {
+ font-weight: $euiFontWeightSemiBold;
+ background-color: $euiFormInputGroupLabelBackground;
+ }
+
+ &:not([class*='isDisabled']) {
+ color: $euiColorDarkShade;
+
+ &:hover,
+ &:focus,
+ &:focus-within {
+ background-color: transparentize($euiFormInputGroupLabelBackground, .5);
+ }
+
+ &:focus,
+ &:focus-within {
+ outline: 2px solid $euiFocusRingColor;
+ }
+ }
+ }
+
+ @each $name, $color in $euiButtonTypes {
+ .euiButtonGroupButton--#{$name}:not([class*='isDisabled']) {
+ &.euiButtonGroupButton-isSelected {
+ @if ($name == 'text') {
+ // The default color is lighter than the normal text color, make the it the text color
+ color: $euiTextColor;
+ } @else {
+ // Other colors need to check their contrast against the page background color.
+ color: makeHighContrastColor($color, $euiPageBackgroundColor);
+ }
+ }
+ }
+ }
+}
diff --git a/src/components/button/button_group/_index.scss b/src/components/button/button_group/_index.scss
index 78cfad351a8..dd9e2fa377b 100644
--- a/src/components/button/button_group/_index.scss
+++ b/src/components/button/button_group/_index.scss
@@ -1 +1,2 @@
@import 'button_group';
+@import 'button_group_button';
diff --git a/src/components/button/button_group/button_group.test.tsx b/src/components/button/button_group/button_group.test.tsx
index 46ffcba1a43..0bfa03159f4 100644
--- a/src/components/button/button_group/button_group.test.tsx
+++ b/src/components/button/button_group/button_group.test.tsx
@@ -19,73 +19,84 @@
import React from 'react';
import { render } from 'enzyme';
-import { requiredProps } from '../../../test';
+import { requiredProps as commonProps } from '../../../test';
-import { EuiButtonGroup, GroupButtonSize } from './button_group';
+import { EuiButtonGroup, EuiButtonGroupProps } from './button_group';
import { COLORS } from '../button';
-const SIZES: GroupButtonSize[] = ['s', 'm', 'compressed'];
+const SIZES: Array = [
+ 's',
+ 'm',
+ 'compressed',
+];
const options = [
{
id: 'button00',
label: 'Option one',
+ iconType: 'bolt',
+ ...commonProps,
},
{
id: 'button01',
label: 'Option two',
+ iconType: 'bolt',
},
{
id: 'button02',
label: 'Option three',
+ iconType: 'bolt',
+ isDisabled: true,
},
];
-describe('EuiButtonGroup', () => {
- test('is rendered', () => {
- const component = render(
- {}} {...requiredProps} />
- );
-
- expect(component).toMatchSnapshot();
- });
-
- describe('props', () => {
- describe('options', () => {
- it('are rendered', () => {
- const component = render(
- {}} options={options} />
- );
+const requiredSingleProps: EuiButtonGroupProps = {
+ type: 'single',
+ legend: 'test',
+ onChange: () => {},
+ options,
+ name: 'test',
+ idSelected: '',
+};
+
+const requiredMultiProps: EuiButtonGroupProps = {
+ type: 'multi',
+ legend: 'test',
+ onChange: () => {},
+ options,
+};
- expect(component).toMatchSnapshot();
- });
-
- it('can pass down data-test-subj', () => {
- const options2 = [
- {
- id: 'button00',
- label: 'Option one',
- 'data-test-subj': 'test',
- },
- ];
+describe('EuiButtonGroup', () => {
+ describe('type', () => {
+ test('single is rendered', () => {
+ const component = render(
+
+ );
- const component = render(
- {}} options={options2} />
- );
+ expect(component).toMatchSnapshot();
+ });
+ test('multi is rendered', () => {
+ const component = render(
+
+ );
- expect(component).toMatchSnapshot();
- });
+ expect(component).toMatchSnapshot();
});
+ });
+ describe('button props', () => {
describe('buttonSize', () => {
SIZES.forEach((size) => {
- test(`${size} is rendered`, () => {
+ test(`${size} is rendered for single`, () => {
+ const component = render(
+
+ );
+
+ expect(component).toMatchSnapshot();
+ });
+ test(`${size} is rendered for multi`, () => {
const component = render(
- {}}
- buttonSize={size}
- options={options}
- />
+
);
expect(component).toMatchSnapshot();
@@ -94,105 +105,89 @@ describe('EuiButtonGroup', () => {
});
describe('isDisabled', () => {
- it('is rendered', () => {
+ it('is rendered for single', () => {
const component = render(
- {}} isDisabled options={options} />
+
);
expect(component).toMatchSnapshot();
});
- });
-
- describe('isFullWidth', () => {
- it('is rendered', () => {
+ it('is rendered for multi', () => {
const component = render(
- {}} isFullWidth options={options} />
+
);
expect(component).toMatchSnapshot();
});
});
- describe('isIconOnly', () => {
- it('is rendered', () => {
+ describe('isFullWidth', () => {
+ it('is rendered for single', () => {
const component = render(
- {}} isIconOnly options={options} />
+
);
expect(component).toMatchSnapshot();
});
- });
-
- describe('color', () => {
- COLORS.forEach((color) => {
- test(`${color} is rendered`, () => {
- const component = render(
- {}}
- color={color}
- options={options}
- />
- );
+ it('is rendered for multi', () => {
+ const component = render(
+
+ );
- expect(component).toMatchSnapshot();
- });
+ expect(component).toMatchSnapshot();
});
});
- describe('legend', () => {
- it('is rendered', () => {
+ describe('isIconOnly', () => {
+ it('is rendered for single', () => {
const component = render(
- {}}
- legend="legend"
- options={options}
- />
+
);
expect(component).toMatchSnapshot();
});
- });
-
- describe('name', () => {
- it('is rendered', () => {
+ it('is rendered for multi', () => {
const component = render(
- {}} name="name" options={options} />
+
);
expect(component).toMatchSnapshot();
});
});
- describe('idSelected', () => {
- it('is rendered', () => {
- const component = render(
- {}}
- idSelected="button00"
- options={options}
- />
- );
+ describe('color', () => {
+ COLORS.forEach((color) => {
+ test(`${color} is rendered for single`, () => {
+ const component = render(
+
+ );
- expect(component).toMatchSnapshot();
+ expect(component).toMatchSnapshot();
+ });
+ test(`${color} is rendered for multi`, () => {
+ const component = render(
+
+ );
+
+ expect(component).toMatchSnapshot();
+ });
});
});
- describe('type of multi', () => {
- it('is rendered', () => {
+ describe('selection', () => {
+ it('idSelected is rendered for single', () => {
const component = render(
- {}} type="multi" options={options} />
+
);
expect(component).toMatchSnapshot();
});
- it('idToSelectedMap is rendered', () => {
+ it('idToSelectedMap is rendered for multi', () => {
const component = render(
{}}
- type="multi"
- idToSelectedMap={{ button00: true, button01: true }}
- options={options}
+ {...requiredMultiProps}
+ idToSelectedMap={{ [options[0].id]: true, [options[1].id]: true }}
/>
);
diff --git a/src/components/button/button_group/button_group.tsx b/src/components/button/button_group/button_group.tsx
index 63750ccf3ce..59d9f5c95f9 100644
--- a/src/components/button/button_group/button_group.tsx
+++ b/src/components/button/button_group/button_group.tsx
@@ -17,148 +17,178 @@
* under the License.
*/
-import React, { ReactNode, FunctionComponent, HTMLAttributes } from 'react';
import classNames from 'classnames';
-
+import React, { FunctionComponent, HTMLAttributes, ReactNode } from 'react';
import { EuiScreenReaderOnly } from '../../accessibility';
-import { ToggleType } from '../../toggle';
-
-import { EuiButtonToggle } from '../button_toggle';
+import { EuiButtonGroupButton } from './button_group_button';
+import { colorToClassNameMap, ButtonColor } from '../button';
+import { EuiButtonContentProps } from '../button_content';
import { CommonProps } from '../../common';
+import { htmlIdGenerator } from '../../../services';
-import { ButtonColor } from '../button';
-import { ButtonContentIconSide } from '../button_content';
-import { IconType } from '../../icon';
-
-export interface EuiButtonGroupIdToSelectedMap {
- [id: string]: boolean;
-}
-
-export type GroupButtonSize = 's' | 'm' | 'compressed';
-
-export interface EuiButtonGroupOption extends CommonProps {
+export interface EuiButtonGroupOptionProps
+ extends EuiButtonContentProps,
+ CommonProps {
+ /**
+ * Each option must have a unique `id` for maintaining selection
+ */
id: string;
+ /**
+ * Each option must have a `label` even for icons which will be applied as the `aria-label`
+ */
label: ReactNode;
- name?: string;
isDisabled?: boolean;
+ /**
+ * The value of the radio input.
+ */
value?: any;
- iconSide?: ButtonContentIconSide;
- iconType?: IconType;
}
-export interface EuiButtonGroupProps extends CommonProps {
- options?: EuiButtonGroupOption[];
- onChange: (id: string, value?: any) => void;
+export type EuiButtonGroupProps = CommonProps & {
/**
* Typical sizing is `s`. Medium `m` size should be reserved for major features.
* `compressed` is meant to be used alongside and within compressed forms.
*/
- buttonSize?: GroupButtonSize;
+ buttonSize?: 's' | 'm' | 'compressed';
isDisabled?: boolean;
+ /**
+ * Expands the whole group to the full width of the container.
+ * Each button gets equal widths no matter the content
+ */
isFullWidth?: boolean;
+ /**
+ * Hides the label to only show the `iconType` provided by the `option`
+ */
isIconOnly?: boolean;
- idSelected?: string;
- legend?: string;
+ /**
+ * A hidden group title (required for accessibility)
+ */
+ legend: string;
+ /**
+ * Compressed styles don't support `ghost` color (Color will be changed to "text")
+ */
color?: ButtonColor;
- name?: string;
- type?: ToggleType;
- idToSelectedMap?: EuiButtonGroupIdToSelectedMap;
-}
+ /**
+ * Actual type is `'single' | 'multi'`.
+ * Determines how the selection of the group should be handled.
+ * With `'single'` only one option can be selected at a time (similar to radio group).
+ * With `'multi'` multiple options selected (similar to checkbox group).
+ */
+ type?: 'single' | 'multi';
+ /**
+ * An array of #EuiButtonGroupOptionProps
+ */
+ options: EuiButtonGroupOptionProps[];
+} & (
+ | {
+ /**
+ * Default for `type` is single so it can also be excluded
+ */
+ type?: 'single';
+ /**
+ * The `name` attribute for radio inputs;
+ * Defaults to a random string
+ */
+ name?: string;
+ /**
+ * Styles the selected option to look selected (usually with `fill`)
+ * Required by and only used in `type='single'`.
+ */
+ idSelected: string;
+ /**
+ * Single: Returns the `id` of the clicked option and the `value`
+ */
+ onChange: (id: string, value?: any) => void;
+ idToSelectedMap?: never;
+ }
+ | {
+ type: 'multi';
+ /**
+ * A map of `id`s as keys with the selected boolean values.
+ * Required by and only used in `type='multi'`.
+ */
+ idToSelectedMap?: { [id: string]: boolean };
+ /**
+ * Multi: Returns the `id` of the clicked option
+ */
+ onChange: (id: string) => void;
+ idSelected?: never;
+ name?: never;
+ }
+ );
-type Props = Omit, 'onChange'> &
+type Props = Omit, 'onChange'> &
EuiButtonGroupProps;
+const groupSizeToClassNameMap = {
+ s: '--small',
+ m: '--medium',
+ compressed: '--compressed',
+};
+
export const EuiButtonGroup: FunctionComponent = ({
className,
buttonSize = 's',
color = 'text',
- idSelected,
+ idSelected = '',
idToSelectedMap = {},
- isDisabled,
- isFullWidth,
- isIconOnly,
- name,
+ isDisabled = false,
+ isFullWidth = false,
+ isIconOnly = false,
legend,
+ name,
onChange,
options = [],
type = 'single',
- 'data-test-subj': dataTestSubj,
...rest
}) => {
+ // Compressed style can't support `ghost` color because it's more like a form field than a button
+ const badColorCombo = buttonSize === 'compressed' && color === 'ghost';
+ const resolvedColor = badColorCombo ? 'text' : color;
+ if (badColorCombo) {
+ console.warn(
+ 'EuiButtonGroup of compressed size does not support the ghost color. It will render as text instead.'
+ );
+ }
+
const classes = classNames(
'euiButtonGroup',
- [`euiButtonGroup--${buttonSize}`],
+ `euiButtonGroup${groupSizeToClassNameMap[buttonSize]}`,
+ `euiButtonGroup${colorToClassNameMap[resolvedColor]}`,
{
'euiButtonGroup--fullWidth': isFullWidth,
+ 'euiButtonGroup--isDisabled': isDisabled,
},
className
);
- const fieldsetClasses = classNames('euiButtonGroup__fieldset', {
- 'euiButtonGroup__fieldset--fullWidth': isFullWidth,
- });
+ const typeIsSingle = type === 'single';
+ const nameIfSingle = name || htmlIdGenerator()();
- let legendNode;
- if (legend) {
- legendNode = (
+ return (
+