diff --git a/packages/components/docs/sass.md b/packages/components/docs/sass.md index 34dde05f1eba..0c662f2754a1 100644 --- a/packages/components/docs/sass.md +++ b/packages/components/docs/sass.md @@ -196,6 +196,7 @@ - [✅highlight [variable]](#highlight-variable) - [✅decorative-01 [variable]](#decorative-01-variable) - [✅hover-light-ui [variable]](#hover-light-ui-variable) + - [✅button-separator [variable]](#button-separator-variable) - [✅skeleton-01 [variable]](#skeleton-01-variable) - [✅skeleton-02 [variable]](#skeleton-02-variable) - [✅⚠️brand-01 [variable]](#brand-01-variable) @@ -4227,6 +4228,7 @@ Define theme variables from a map of tokens $highlight: map-get($theme, 'highlight') !global; $decorative-01: map-get($theme, 'decorative-01') !global; $hover-light-ui: map-get($theme, 'hover-light-ui') !global; + $button-separator: map-get($theme, 'button-separator') !global; $skeleton-01: map-get($theme, 'skeleton-01') !global; $skeleton-02: map-get($theme, 'skeleton-02') !global; $brand-01: map-get($theme, 'brand-01') !global; @@ -4539,6 +4541,10 @@ Define theme variables from a map of tokens --#{$custom-property-prefix}-hover-light-ui, map-get($theme, 'hover-light-ui') ) !global; + $button-separator: var( + --#{$custom-property-prefix}-button-separator, + map-get($theme, 'button-separator') + ) !global; $skeleton-01: var( --#{$custom-property-prefix}-skeleton-01, map-get($theme, 'skeleton-01') @@ -5232,6 +5238,19 @@ Define theme variables from a map of tokens ); } + @if should-emit( + $theme, + $parent-carbon-theme, + 'button-separator', + $emit-difference + ) + { + @include custom-property( + 'button-separator', + map-get($theme, 'button-separator') + ); + } + @if should-emit( $theme, $parent-carbon-theme, @@ -6015,6 +6034,7 @@ Define theme variables from a map of tokens - [highlight [variable]](#highlight-variable) - [decorative-01 [variable]](#decorative-01-variable) - [hover-light-ui [variable]](#hover-light-ui-variable) + - [button-separator [variable]](#button-separator-variable) - [skeleton-01 [variable]](#skeleton-01-variable) - [skeleton-02 [variable]](#skeleton-02-variable) - [brand-01 [variable]](#brand-01-variable) @@ -6171,6 +6191,7 @@ $carbon--theme--g90: map-merge( highlight: #0043ce, decorative-01: #6f6f6f, hover-light-ui: #6f6f6f, + button-separator: #161616, skeleton-01: #353535, skeleton-02: #525252, brand-02: #6f6f6f, @@ -6246,6 +6267,7 @@ $carbon--theme--g100: map-merge( highlight: #002d9c, decorative-01: #525252, hover-light-ui: #525252, + button-separator: #161616, skeleton-01: #353535, skeleton-02: #393939, brand-02: #6f6f6f, @@ -6412,6 +6434,7 @@ $carbon--theme: ( highlight: if(global-variable-exists('highlight'), $highlight, map-get($carbon--theme--white, 'highlight')), decorative-01: if(global-variable-exists('decorative-01'), $decorative-01, map-get($carbon--theme--white, 'decorative-01')), hover-light-ui: if(global-variable-exists('hover-light-ui'), $hover-light-ui, map-get($carbon--theme--white, 'hover-light-ui')), + button-separator: if(global-variable-exists('button-separator'), $button-separator, map-get($carbon--theme--white, 'button-separator')), skeleton-01: if(global-variable-exists('skeleton-01'), $skeleton-01, map-get($carbon--theme--white, 'skeleton-01')), skeleton-02: if(global-variable-exists('skeleton-02'), $skeleton-02, map-get($carbon--theme--white, 'skeleton-02')), brand-01: if(global-variable-exists('brand-01'), $brand-01, map-get($carbon--theme--white, 'brand-01')), @@ -8327,6 +8350,30 @@ $hover-light-ui: if( - [carbon--theme [mixin]](#carbon--theme-mixin) - [content-switcher [mixin]](#content-switcher-mixin) +### ✅button-separator [variable] + +
+Source code + +```scss +$button-separator: if( + global-variable-exists('carbon--theme') and map-has-key( + $carbon--theme, + 'button-separator' + ), + map-get($carbon--theme, 'button-separator'), + #e0e0e0 +); +``` + +
+ +- **Group**: [@carbon/themes](#carbonthemes) +- **Type**: `{undefined}` +- **Used by**: + - [carbon--theme [mixin]](#carbon--theme-mixin) + - [button [mixin]](#button-mixin) + ### ✅skeleton-01 [variable] Skeleton state of graphics @@ -13823,17 +13870,43 @@ Button styles display: flex; } - .#{$prefix}--btn-set > .#{$prefix}--btn { + .#{$prefix}--btn-set--stacked { + flex-direction: column; + } + + .#{$prefix}--btn-set .#{$prefix}--btn { width: 100%; // 196px from design kit max-width: rem(196px); + + &:not(:first-of-type):not(:focus) { + box-shadow: rem(-1px) 0 0 0 $button-separator; + } } - .#{$prefix}--btn--secondary.#{$prefix}--btn--disabled - + .#{$prefix}--btn--primary.#{$prefix}--btn--disabled, - .#{$prefix}--btn--tertiary.#{$prefix}--btn--disabled - + .#{$prefix}--btn--danger.#{$prefix}--btn--disabled { + .#{$prefix}--btn-set .#{$prefix}--btn:focus + .#{$prefix}--btn { + box-shadow: inherit; + } + + .#{$prefix}--btn-set--stacked + .#{$prefix}--btn:not(:first-of-type):not(:focus) { + box-shadow: 0 rem(-1px) 0 0 $button-separator; + } + + .#{$prefix}--btn-set .#{$prefix}--btn.#{$prefix}--btn--disabled { box-shadow: rem(-1px) 0 0 0 $disabled-03; + + &:first-of-type { + box-shadow: none; + } + } + + .#{$prefix}--btn-set--stacked .#{$prefix}--btn.#{$prefix}--btn--disabled { + box-shadow: 0 rem(-1px) 0 0 $disabled-03; + + &:first-of-type { + box-shadow: none; + } } .#{$prefix}--btn { @@ -14091,6 +14164,7 @@ Button styles - [button-base [mixin]](#button-base-mixin) - [button-theme [mixin]](#button-theme-mixin) - [prefix [variable]](#prefix-variable) + - [button-separator [variable]](#button-separator-variable) - [disabled-03 [variable]](#disabled-03-variable) - [interactive-01 [variable]](#interactive-01-variable) - [text-04 [variable]](#text-04-variable) diff --git a/packages/components/src/components/button/_button.scss b/packages/components/src/components/button/_button.scss index c5862dccadb4..6554803a7dad 100644 --- a/packages/components/src/components/button/_button.scss +++ b/packages/components/src/components/button/_button.scss @@ -23,17 +23,50 @@ display: flex; } - .#{$prefix}--btn-set > .#{$prefix}--btn { + .#{$prefix}--btn-set--stacked { + flex-direction: column; + } + + .#{$prefix}--btn-set .#{$prefix}--btn { width: 100%; // 196px from design kit max-width: rem(196px); + + &:not(:focus) { + box-shadow: rem(-1px) 0 0 0 $button-separator; + } + + &:first-of-type:not(:focus) { + box-shadow: inherit; + } + } + + .#{$prefix}--btn-set .#{$prefix}--btn:focus + .#{$prefix}--btn { + box-shadow: inherit; + } + + .#{$prefix}--btn-set--stacked .#{$prefix}--btn:not(:focus) { + box-shadow: 0 rem(-1px) 0 0 $button-separator; + } + + .#{$prefix}--btn-set--stacked .#{$prefix}--btn:first-of-type:not(:focus) { + box-shadow: inherit; } - .#{$prefix}--btn--secondary.#{$prefix}--btn--disabled - + .#{$prefix}--btn--primary.#{$prefix}--btn--disabled, - .#{$prefix}--btn--tertiary.#{$prefix}--btn--disabled - + .#{$prefix}--btn--danger.#{$prefix}--btn--disabled { + .#{$prefix}--btn-set .#{$prefix}--btn.#{$prefix}--btn--disabled { box-shadow: rem(-1px) 0 0 0 $disabled-03; + + &:first-of-type { + box-shadow: none; + } + } + + .#{$prefix}--btn-set--stacked .#{$prefix}--btn.#{$prefix}--btn--disabled { + box-shadow: 0 rem(-1px) 0 0 $disabled-03; + + &:first-of-type { + box-shadow: none; + } } .#{$prefix}--btn { diff --git a/packages/elements/docs/sass.md b/packages/elements/docs/sass.md index f0694bab46c4..1e33ac99c836 100644 --- a/packages/elements/docs/sass.md +++ b/packages/elements/docs/sass.md @@ -196,6 +196,7 @@ - [✅highlight [variable]](#highlight-variable) - [✅decorative-01 [variable]](#decorative-01-variable) - [✅hover-light-ui [variable]](#hover-light-ui-variable) + - [✅button-separator [variable]](#button-separator-variable) - [✅skeleton-01 [variable]](#skeleton-01-variable) - [✅skeleton-02 [variable]](#skeleton-02-variable) - [✅⚠️brand-01 [variable]](#brand-01-variable) @@ -3848,6 +3849,7 @@ Define theme variables from a map of tokens $highlight: map-get($theme, 'highlight') !global; $decorative-01: map-get($theme, 'decorative-01') !global; $hover-light-ui: map-get($theme, 'hover-light-ui') !global; + $button-separator: map-get($theme, 'button-separator') !global; $skeleton-01: map-get($theme, 'skeleton-01') !global; $skeleton-02: map-get($theme, 'skeleton-02') !global; $brand-01: map-get($theme, 'brand-01') !global; @@ -4160,6 +4162,10 @@ Define theme variables from a map of tokens --#{$custom-property-prefix}-hover-light-ui, map-get($theme, 'hover-light-ui') ) !global; + $button-separator: var( + --#{$custom-property-prefix}-button-separator, + map-get($theme, 'button-separator') + ) !global; $skeleton-01: var( --#{$custom-property-prefix}-skeleton-01, map-get($theme, 'skeleton-01') @@ -4853,6 +4859,19 @@ Define theme variables from a map of tokens ); } + @if should-emit( + $theme, + $parent-carbon-theme, + 'button-separator', + $emit-difference + ) + { + @include custom-property( + 'button-separator', + map-get($theme, 'button-separator') + ); + } + @if should-emit( $theme, $parent-carbon-theme, @@ -5636,6 +5655,7 @@ Define theme variables from a map of tokens - [highlight [variable]](#highlight-variable) - [decorative-01 [variable]](#decorative-01-variable) - [hover-light-ui [variable]](#hover-light-ui-variable) + - [button-separator [variable]](#button-separator-variable) - [skeleton-01 [variable]](#skeleton-01-variable) - [skeleton-02 [variable]](#skeleton-02-variable) - [brand-01 [variable]](#brand-01-variable) @@ -5792,6 +5812,7 @@ $carbon--theme--g90: map-merge( highlight: #0043ce, decorative-01: #6f6f6f, hover-light-ui: #6f6f6f, + button-separator: #161616, skeleton-01: #353535, skeleton-02: #525252, brand-02: #6f6f6f, @@ -5867,6 +5888,7 @@ $carbon--theme--g100: map-merge( highlight: #002d9c, decorative-01: #525252, hover-light-ui: #525252, + button-separator: #161616, skeleton-01: #353535, skeleton-02: #393939, brand-02: #6f6f6f, @@ -6033,6 +6055,7 @@ $carbon--theme: ( highlight: if(global-variable-exists('highlight'), $highlight, map-get($carbon--theme--white, 'highlight')), decorative-01: if(global-variable-exists('decorative-01'), $decorative-01, map-get($carbon--theme--white, 'decorative-01')), hover-light-ui: if(global-variable-exists('hover-light-ui'), $hover-light-ui, map-get($carbon--theme--white, 'hover-light-ui')), + button-separator: if(global-variable-exists('button-separator'), $button-separator, map-get($carbon--theme--white, 'button-separator')), skeleton-01: if(global-variable-exists('skeleton-01'), $skeleton-01, map-get($carbon--theme--white, 'skeleton-01')), skeleton-02: if(global-variable-exists('skeleton-02'), $skeleton-02, map-get($carbon--theme--white, 'skeleton-02')), brand-01: if(global-variable-exists('brand-01'), $brand-01, map-get($carbon--theme--white, 'brand-01')), @@ -7599,6 +7622,29 @@ $hover-light-ui: if( +- **Group**: [@carbon/themes](#carbonthemes) +- **Type**: `{undefined}` +- **Used by**: + - [carbon--theme [mixin]](#carbon--theme-mixin) + +### ✅button-separator [variable] + +
+Source code + +```scss +$button-separator: if( + global-variable-exists('carbon--theme') and map-has-key( + $carbon--theme, + 'button-separator' + ), + map-get($carbon--theme, 'button-separator'), + #e0e0e0 +); +``` + +
+ - **Group**: [@carbon/themes](#carbonthemes) - **Type**: `{undefined}` - **Used by**: diff --git a/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap b/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap index a4923da640e9..d912b269c735 100644 --- a/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap +++ b/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap @@ -216,6 +216,22 @@ Map { }, "render": [Function], }, + "ButtonSet" => Object { + "$$typeof": Symbol(react.forward_ref), + "displayName": "ButtonSet", + "propTypes": Object { + "children": Object { + "type": "node", + }, + "className": Object { + "type": "string", + }, + "stacked": Object { + "type": "bool", + }, + }, + "render": [Function], + }, "Checkbox" => Object { "$$typeof": Symbol(react.forward_ref), "defaultProps": Object { diff --git a/packages/react/src/__tests__/index-test.js b/packages/react/src/__tests__/index-test.js index 3b62bea82004..180afdd51a7b 100644 --- a/packages/react/src/__tests__/index-test.js +++ b/packages/react/src/__tests__/index-test.js @@ -22,6 +22,7 @@ describe('Carbon Components React', () => { "BreadcrumbItem", "BreadcrumbSkeleton", "Button", + "ButtonSet", "ButtonSkeleton", "Checkbox", "CheckboxSkeleton", diff --git a/packages/react/src/components/Button/Button-story.js b/packages/react/src/components/Button/Button-story.js index c20068ab73e4..1f29feb8279a 100644 --- a/packages/react/src/components/Button/Button-story.js +++ b/packages/react/src/components/Button/Button-story.js @@ -9,13 +9,11 @@ import React from 'react'; import { storiesOf } from '@storybook/react'; import { action } from '@storybook/addon-actions'; import { withKnobs, boolean, select, text } from '@storybook/addon-knobs'; -import { settings } from 'carbon-components'; import { iconAddSolid, iconSearch } from 'carbon-icons'; import { Add16, AddFilled16, Search16 } from '@carbon/icons-react'; import Button from '../Button'; import ButtonSkeleton from '../Button/Button.Skeleton'; - -const { prefix } = settings; +import ButtonSet from '../ButtonSet'; const icons = { None: 'None', @@ -109,6 +107,7 @@ const props = { 'Icon description (iconDescription)', 'Button icon' ), + stacked: boolean('Stack buttons vertically (stacked)', false), onClick: action('onClick'), onFocus: action('onFocus'), }; @@ -197,22 +196,22 @@ storiesOf('Button', module) .add( 'Sets of Buttons', () => { - const setProps = props.set(); + const { stacked, ...buttonProps } = props.set(); return ( -
- - -
+ ); }, { info: { text: ` - When an action required by the user has more than one option, always use a a negative action button (secondary) paired with a positive action button (primary) in that order. Negative action buttons will be on the left. Positive action buttons should be on the right. When these two types buttons are paired in the correct order, they will automatically space themselves apart. + When an action required by the user has more than one option, always use a negative action button (secondary) paired with a positive action button (primary) in that order. Negative action buttons will be on the left. Positive action buttons should be on the right. When these two types buttons are paired in the correct order, they will automatically space themselves apart. `, }, } diff --git a/packages/react/src/components/ButtonSet/ButtonSet-test.js b/packages/react/src/components/ButtonSet/ButtonSet-test.js new file mode 100644 index 000000000000..80f75bbebce0 --- /dev/null +++ b/packages/react/src/components/ButtonSet/ButtonSet-test.js @@ -0,0 +1,49 @@ +/** + * Copyright IBM Corp. 2016, 2018 + * + * This source code is licensed under the Apache-2.0 license found in the + * LICENSE file in the root directory of this source tree. + */ + +import React from 'react'; +import ButtonSet from '../ButtonSet'; +import { shallow } from 'enzyme'; +import { settings } from 'carbon-components'; + +const { prefix } = settings; + +describe('ButtonSet', () => { + let wrapper; + + beforeEach(() => { + wrapper = shallow(); + }); + + it('should render empty set as expected', () => { + expect(wrapper.find('.child').length).toBe(0); + }); + + it('should render nonempty set as expected', () => { + wrapper = shallow( + +
+
+ + ); + expect(wrapper.find('.test-child').length).toBe(2); + }); + + it('should render wrapper as expected', () => { + expect(wrapper.length).toBe(1); + }); + + it('should have the expected classes in a horizontal set', () => { + expect(wrapper.hasClass(`${prefix}--btn-set`)).toEqual(true); + }); + + it('should have the expected classes in a vertical set', () => { + wrapper.setProps({ stacked: true }); + expect(wrapper.hasClass(`${prefix}--btn-set`)).toEqual(true); + expect(wrapper.hasClass(`${prefix}--btn-set--stacked`)).toEqual(true); + }); +}); diff --git a/packages/react/src/components/ButtonSet/ButtonSet.js b/packages/react/src/components/ButtonSet/ButtonSet.js new file mode 100644 index 000000000000..38b671dfc8e3 --- /dev/null +++ b/packages/react/src/components/ButtonSet/ButtonSet.js @@ -0,0 +1,47 @@ +/** + * Copyright IBM Corp. 2016, 2018 + * + * This source code is licensed under the Apache-2.0 license found in the + * LICENSE file in the root directory of this source tree. + */ + +import React from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; +import { settings } from 'carbon-components'; + +const { prefix } = settings; +const ButtonSet = React.forwardRef(function ButtonSet( + { children, className, stacked, ...rest }, + ref +) { + const buttonSetClasses = classNames(className, `${prefix}--btn-set`, { + [`${prefix}--btn-set--stacked`]: stacked, + }); + return ( +
+ {children} +
+ ); +}); + +ButtonSet.displayName = 'ButtonSet'; +ButtonSet.propTypes = { + /** + * Specify the content of your ButtonSet + */ + children: PropTypes.node, + + /** + * Specify an optional className to be added to your ButtonSet + */ + className: PropTypes.string, + + /** + * Specify the button arrangement of the set (vertically stacked or + * horizontal) + */ + stacked: PropTypes.bool, +}; + +export default ButtonSet; diff --git a/packages/react/src/components/ButtonSet/index.js b/packages/react/src/components/ButtonSet/index.js new file mode 100644 index 000000000000..c25ef0671462 --- /dev/null +++ b/packages/react/src/components/ButtonSet/index.js @@ -0,0 +1,8 @@ +/** + * Copyright IBM Corp. 2016, 2018 + * + * This source code is licensed under the Apache-2.0 license found in the + * LICENSE file in the root directory of this source tree. + */ + +export default from './ButtonSet'; diff --git a/packages/react/src/components/ComposedModal/ComposedModal.js b/packages/react/src/components/ComposedModal/ComposedModal.js index d8015bed3c69..cc4bd48aac52 100644 --- a/packages/react/src/components/ComposedModal/ComposedModal.js +++ b/packages/react/src/components/ComposedModal/ComposedModal.js @@ -8,6 +8,7 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import Button from '../Button'; +import ButtonSet from '../ButtonSet'; import classNames from 'classnames'; import { settings } from 'carbon-components'; import { Close20 } from '@carbon/icons-react'; @@ -575,7 +576,7 @@ export class ModalFooter extends Component { }); return ( -
+ {secondaryButtonText && ( @@ -430,7 +431,7 @@ export default class Modal extends Component { ref={this.button}> {primaryButtonText} -
+ )}
); diff --git a/packages/react/src/components/ModalWrapper/__snapshots__/ModalWrapper-test.js.snap b/packages/react/src/components/ModalWrapper/__snapshots__/ModalWrapper-test.js.snap index cd00e70a0460..3aece0432c3e 100644 --- a/packages/react/src/components/ModalWrapper/__snapshots__/ModalWrapper-test.js.snap +++ b/packages/react/src/components/ModalWrapper/__snapshots__/ModalWrapper-test.js.snap @@ -149,46 +149,50 @@ exports[`ModalWrapper should render 1`] = ` Text

-
- - - + + - -
+ + + +
+- **Group**: [@carbon/themes](#carbonthemes) +- **Type**: `{undefined}` +- **Used by**: + - [carbon--theme [mixin]](#carbon--theme-mixin) + +### ✅button-separator [variable] + +
+Source code + +```scss +$button-separator: if( + global-variable-exists('carbon--theme') and map-has-key( + $carbon--theme, + 'button-separator' + ), + map-get($carbon--theme, 'button-separator'), + #e0e0e0 +); +``` + +
+ - **Group**: [@carbon/themes](#carbonthemes) - **Type**: `{undefined}` - **Used by**: diff --git a/packages/themes/src/g10.js b/packages/themes/src/g10.js index a0d7f0bcb840..78832b7a2b77 100644 --- a/packages/themes/src/g10.js +++ b/packages/themes/src/g10.js @@ -130,6 +130,8 @@ export const decorative01 = gray20; export const hoverLightUI = '#e5e5e5'; +export const buttonSeparator = '#e0e0e0'; + export const skeleton01 = '#e5e5e5'; export const skeleton02 = gray30; diff --git a/packages/themes/src/g100.js b/packages/themes/src/g100.js index 5014ba6c087d..0dbcd2509378 100644 --- a/packages/themes/src/g100.js +++ b/packages/themes/src/g100.js @@ -129,6 +129,8 @@ export const decorative01 = gray70; export const hoverLightUI = '#525252'; +export const buttonSeparator = '#161616'; + export const skeleton01 = '#353535'; export const skeleton02 = gray80; diff --git a/packages/themes/src/g90.js b/packages/themes/src/g90.js index ea31594bf3ad..918691d02a51 100644 --- a/packages/themes/src/g90.js +++ b/packages/themes/src/g90.js @@ -131,6 +131,8 @@ export const decorative01 = gray60; export const hoverLightUI = '#6f6f6f'; +export const buttonSeparator = '#161616'; + export const skeleton01 = '#353535'; export const skeleton02 = gray70; diff --git a/packages/themes/src/tokens.js b/packages/themes/src/tokens.js index caeafb655511..35d60acbbb20 100644 --- a/packages/themes/src/tokens.js +++ b/packages/themes/src/tokens.js @@ -99,6 +99,8 @@ const colors = [ 'hoverLightUI', + 'buttonSeparator', + 'skeleton01', 'skeleton02', @@ -233,6 +235,7 @@ export const unstable__meta = { 'hoverField', 'decorative01', 'hoverLightUI', + 'buttonSeparator', ], }, ], diff --git a/packages/themes/src/v9.js b/packages/themes/src/v9.js index 679c71e6e100..407e2469e488 100644 --- a/packages/themes/src/v9.js +++ b/packages/themes/src/v9.js @@ -94,6 +94,8 @@ export const decorative01 = '#EEF4FC'; export const hoverLightUI = '#EEF4FC'; +export const buttonSeparator = '#e0e0e0'; + export const skeleton01 = 'rgba(61, 112, 178, .1)'; export const skeleton02 = 'rgba(61, 112, 178, .1)'; diff --git a/packages/themes/src/white.js b/packages/themes/src/white.js index 45d58dc58450..b933910cdced 100644 --- a/packages/themes/src/white.js +++ b/packages/themes/src/white.js @@ -130,6 +130,8 @@ export const decorative01 = gray20; export const hoverLightUI = '#e5e5e5'; +export const buttonSeparator = '#e0e0e0'; + export const skeleton01 = '#e5e5e5'; export const skeleton02 = gray30;