diff --git a/packages/carbon-react/__tests__/index-test.js b/packages/carbon-react/__tests__/index-test.js index 431378548312..aaf72d307b15 100644 --- a/packages/carbon-react/__tests__/index-test.js +++ b/packages/carbon-react/__tests__/index-test.js @@ -62,6 +62,7 @@ Array [ "FormItem", "FormLabel", "Grid", + "HStack", "Header", "HeaderContainer", "HeaderGlobalAction", @@ -134,6 +135,7 @@ Array [ "SkipToContent", "Slider", "SliderSkeleton", + "Stack", "StructuredListBody", "StructuredListCell", "StructuredListHead", @@ -197,6 +199,7 @@ Array [ "TooltipDefinition", "TooltipIcon", "UnorderedList", + "VStack", "unstable_Heading", "unstable_PageSelector", "unstable_Pagination", diff --git a/packages/carbon-react/src/index.js b/packages/carbon-react/src/index.js index 4d19bb1c448e..35d1a7422c72 100644 --- a/packages/carbon-react/src/index.js +++ b/packages/carbon-react/src/index.js @@ -196,6 +196,9 @@ export { unstable_useContextMenu, unstable_Heading, unstable_Section, + unstable_HStack as HStack, + unstable_Stack as Stack, + unstable_VStack as VStack, } from 'carbon-components-react'; export { diff --git a/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap b/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap index 836867e06dc5..1f5a0926cbd3 100644 --- a/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap +++ b/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap @@ -8938,5 +8938,66 @@ Map { }, }, "unstable_usePrefix" => Object {}, + "unstable_HStack" => Object { + "$$typeof": Symbol(react.forward_ref), + "render": [Function], + }, + "unstable_Stack" => Object { + "$$typeof": Symbol(react.forward_ref), + "propTypes": Object { + "as": Object { + "type": "elementType", + }, + "children": Object { + "type": "node", + }, + "className": Object { + "type": "string", + }, + "gap": Object { + "args": Array [ + Array [ + Object { + "type": "string", + }, + Object { + "args": Array [ + Array [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + ], + ], + "type": "oneOf", + }, + ], + ], + "type": "oneOfType", + }, + "orientation": Object { + "args": Array [ + Array [ + "horizontal", + "vertical", + ], + ], + "type": "oneOf", + }, + }, + "render": [Function], + }, + "unstable_VStack" => Object { + "$$typeof": Symbol(react.forward_ref), + "render": [Function], + }, } `; diff --git a/packages/react/package.json b/packages/react/package.json index ea509eaca1fb..6c14b5379379 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -49,6 +49,7 @@ "@babel/runtime": "^7.14.6", "@carbon/feature-flags": "^0.6.0", "@carbon/icons-react": "^10.41.0", + "@carbon/layout": "^10.33.0", "@carbon/telemetry": "0.0.0-alpha.6", "classnames": "2.3.1", "copy-to-clipboard": "^3.3.1", diff --git a/packages/react/src/__tests__/index-test.js b/packages/react/src/__tests__/index-test.js index b78efd31b253..da41c1401283 100644 --- a/packages/react/src/__tests__/index-test.js +++ b/packages/react/src/__tests__/index-test.js @@ -200,6 +200,7 @@ Array [ "TooltipIcon", "UnorderedList", "unstable_FeatureFlags", + "unstable_HStack", "unstable_Heading", "unstable_Menu", "unstable_MenuDivider", @@ -211,8 +212,10 @@ Array [ "unstable_Pagination", "unstable_ProgressBar", "unstable_Section", + "unstable_Stack", "unstable_TreeNode", "unstable_TreeView", + "unstable_VStack", "unstable_useContextMenu", "unstable_useFeatureFlag", "unstable_useFeatureFlags", diff --git a/packages/react/src/components/Stack/Stack.js b/packages/react/src/components/Stack/Stack.js new file mode 100644 index 000000000000..23c9b0fca0ef --- /dev/null +++ b/packages/react/src/components/Stack/Stack.js @@ -0,0 +1,100 @@ +/** + * 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 { spacing } from '@carbon/layout'; +import cx from 'classnames'; +import PropTypes from 'prop-types'; +import React from 'react'; +import { usePrefix } from '../../internal/usePrefix'; + +/** + * The steps in the spacing scale + * @type {Array} + */ +const SPACING_STEPS = Array.from({ length: spacing.length - 1 }).map( + (_, step) => { + return step + 1; + } +); + +/** + * The Stack component is a useful layout utility in a component-based model. + * This allows components to not use margin and instead delegate the + * responsibility of positioning and layout to parent components. + * + * In the case of the Stack component, it uses the spacing scale from the + * Design Language in order to determine how much space there should be between + * items rendered by the Stack component. It also supports a custom `gap` prop + * which will allow a user to provide a custom value for the gap of the layout. + * + * This component supports both horizontal and vertical orientations. + * + * Inspiration for this component: + * + * - https://paste.twilio.design/layout/stack/ + * - https://github.com/Workday/canvas-kit/blob/f2f599654876700f483a1d8c5de82a41315c76f1/modules/labs-react/layout/lib/Stack.tsx + */ +const Stack = React.forwardRef(function Stack(props, ref) { + const { + as: BaseComponent = 'div', + children, + className: customClassName, + gap, + orientation = 'vertical', + ...rest + } = props; + const prefix = usePrefix(); + const className = cx(customClassName, { + [`${prefix}--stack-${orientation}`]: true, + [`${prefix}--stack-scale-${gap}`]: typeof gap === 'number', + }); + const style = {}; + + if (typeof gap === 'string') { + style[`--${prefix}-stack-gap`] = gap; + } + + return ( + + {children} + + ); +}); + +Stack.propTypes = { + /** + * Provide a custom element type to render as the outermost element in + * the Stack component. By default, this component will render a `div`. + */ + as: PropTypes.elementType, + + /** + * Provide the elements that will be rendered as children inside of the Stack + * component. These elements will have having spacing between them according + * to the `step` and `orientation` prop + */ + children: PropTypes.node, + + /** + * Provide a custom class name to be used by the outermost element rendered by + * Stack + */ + className: PropTypes.string, + + /** + * Provide either a custom value or a step from the spacing scale to be used + * as the gap in the layout + */ + gap: PropTypes.oneOfType([PropTypes.string, PropTypes.oneOf(SPACING_STEPS)]), + + /** + * Specify the orientation of them items in the Stack + */ + orientation: PropTypes.oneOf(['horizontal', 'vertical']), +}; + +export { Stack }; diff --git a/packages/react/src/components/Stack/__tests__/Stack-test.e2e.js b/packages/react/src/components/Stack/__tests__/Stack-test.e2e.js new file mode 100644 index 000000000000..31d2eea44d03 --- /dev/null +++ b/packages/react/src/components/Stack/__tests__/Stack-test.e2e.js @@ -0,0 +1,74 @@ +/** + * 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 '@carbon/styles/scss/components/stack/_index.scss'; + +import { mount } from '@cypress/react'; +import { spacing } from '@carbon/layout'; +import React from 'react'; +import { Stack } from '../../Stack'; +import { PrefixContext } from '../../../internal/usePrefix'; + +const SPACING_STEPS = Array.from({ length: spacing.length - 1 }).map( + (_, step) => { + return step + 1; + } +); + +describe('Stack', () => { + it('should default to the vertical orientation', () => { + mount( + + {SPACING_STEPS.map((step) => { + return ( + +
item 1
+
item 2
+
item 3
+
+ ); + })} +
+ ); + + cy.percySnapshot(); + }); + + it('should support a horizontal orientation', () => { + mount( + + {SPACING_STEPS.map((step) => { + return ( +
+ +
item 1
+
item 2
+
item 3
+
+
+ ); + })} +
+ ); + + cy.percySnapshot(); + }); + + it('should support a custom gap with the `gap` prop', () => { + mount( + + +
item 1
+
item 2
+
item 3
+
+
+ ); + + cy.percySnapshot(); + }); +}); diff --git a/packages/react/src/components/Stack/__tests__/Stack-test.js b/packages/react/src/components/Stack/__tests__/Stack-test.js new file mode 100644 index 000000000000..939b6c153a25 --- /dev/null +++ b/packages/react/src/components/Stack/__tests__/Stack-test.js @@ -0,0 +1,88 @@ +/** + * 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 { render } from '@testing-library/react'; +import React from 'react'; +import { HStack, Stack, VStack } from '../../Stack'; + +describe('Stack', () => { + it('should support alternate element types with the `as` prop', () => { + const { container } = render( + +
one
+
two
+
three
+
+ ); + + expect(container.firstChild.tagName).toBe('SECTION'); + }); + + it('should support a custom className with the `className` prop', () => { + const { container } = render( + +
one
+
two
+
three
+
+ ); + + expect(container.firstChild).toHaveClass('test'); + }); + + it('should apply additional props to the outermost element', () => { + const { container } = render( + +
one
+
two
+
three
+
+ ); + + expect(container.firstChild).toHaveAttribute('data-testid', 'test'); + }); + + it('should forward the given ref to the outermost element', () => { + const ref = jest.fn(); + const { container } = render( + +
one
+
two
+
three
+
+ ); + expect(ref).toHaveBeenCalledWith(container.firstChild); + }); + + describe('HStack', () => { + it('should forward the given ref to the outermost element', () => { + const ref = jest.fn(); + const { container } = render( + +
one
+
two
+
three
+
+ ); + expect(ref).toHaveBeenCalledWith(container.firstChild); + }); + }); + + describe('VStack', () => { + it('should forward the given ref to the outermost element', () => { + const ref = jest.fn(); + const { container } = render( + +
one
+
two
+
three
+
+ ); + expect(ref).toHaveBeenCalledWith(container.firstChild); + }); + }); +}); diff --git a/packages/react/src/components/Stack/index.js b/packages/react/src/components/Stack/index.js new file mode 100644 index 000000000000..e7df40491a6a --- /dev/null +++ b/packages/react/src/components/Stack/index.js @@ -0,0 +1,19 @@ +/** + * 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 { Stack } from './Stack'; + +const HStack = React.forwardRef(function HStack(props, ref) { + return ; +}); + +const VStack = React.forwardRef(function VStack(props, ref) { + return ; +}); + +export { HStack, Stack, VStack }; diff --git a/packages/react/src/components/Stack/next/Stack.stories.js b/packages/react/src/components/Stack/next/Stack.stories.js new file mode 100644 index 000000000000..bb9857ba2ba4 --- /dev/null +++ b/packages/react/src/components/Stack/next/Stack.stories.js @@ -0,0 +1,73 @@ +/** + * 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 { Stack } from '../Stack'; + +export default { + title: 'Layout/Stack', + component: Stack, + parameters: { + controls: { + hideNoControlsWarning: true, + }, + }, + argTypes: { + children: { + table: { + disable: true, + }, + }, + }, +}; + +export const Default = () => { + return ( + +
Item 1
+
Item 2
+
Item 3
+
+ ); +}; + +export const Horizontal = () => { + return ( + +
Item 1
+
Item 2
+
Item 3
+
+ ); +}; + +const PlaygroundStory = (props) => { + return ( + +
Item 1
+
Item 2
+
Item 3
+
+ ); +}; + +export const Playground = PlaygroundStory.bind({}); + +Playground.argTypes = { + gap: { + options: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + control: { + type: 'select', + }, + }, + orientation: { + options: ['horizontal', 'vertical'], + control: { + type: 'select', + }, + }, +}; diff --git a/packages/react/src/index.js b/packages/react/src/index.js index 4b81f0fd6e85..c00575c79cfd 100644 --- a/packages/react/src/index.js +++ b/packages/react/src/index.js @@ -232,3 +232,8 @@ export { } from './components/Heading'; export { default as unstable_ProgressBar } from './components/ProgressBar'; export { usePrefix as unstable_usePrefix } from './internal/usePrefix'; +export { + HStack as unstable_HStack, + Stack as unstable_Stack, + VStack as unstable_VStack, +} from './components/Stack'; diff --git a/packages/styles/scss/_spacing.scss b/packages/styles/scss/_spacing.scss index 162b516517f6..6eb1c72f8012 100644 --- a/packages/styles/scss/_spacing.scss +++ b/packages/styles/scss/_spacing.scss @@ -7,6 +7,7 @@ @forward '@carbon/layout/scss/modules/spacing' show + $spacing, $spacing-01, $spacing-02, $spacing-03, diff --git a/packages/styles/scss/components/_index.scss b/packages/styles/scss/components/_index.scss index be53e497d243..e9113da0e260 100644 --- a/packages/styles/scss/components/_index.scss +++ b/packages/styles/scss/components/_index.scss @@ -43,6 +43,7 @@ @use 'select'; @use 'skeleton-styles'; @use 'slider'; +@use 'stack'; @use 'structured-list'; @use 'tabs'; @use 'tag'; diff --git a/packages/styles/scss/components/stack/_index.scss b/packages/styles/scss/components/stack/_index.scss new file mode 100644 index 000000000000..7dc179a04b33 --- /dev/null +++ b/packages/styles/scss/components/stack/_index.scss @@ -0,0 +1,11 @@ +// +// Copyright IBM Corp. 2018, 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. +// + +@forward 'stack'; +@use 'stack'; + +@include stack.stack; diff --git a/packages/styles/scss/components/stack/_stack.scss b/packages/styles/scss/components/stack/_stack.scss new file mode 100644 index 000000000000..bc6b4ac20691 --- /dev/null +++ b/packages/styles/scss/components/stack/_stack.scss @@ -0,0 +1,34 @@ +// +// Copyright IBM Corp. 2018, 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. +// + +@use '../../config' as *; +@use '../../spacing'; +@use '../../utilities/custom-property'; + +@mixin stack() { + .#{$prefix}--stack-horizontal { + display: inline-grid; + column-gap: custom-property.get-var('stack-gap', 0); + grid-auto-flow: column; + } + + .#{$prefix}--stack-vertical { + display: grid; + grid-auto-flow: row; + row-gap: custom-property.get-var('stack-gap', 0); + } + + $index: 1; + + @each $step, $value in spacing.$spacing { + .#{$prefix}--stack-scale-#{$index} { + @include custom-property.declaration('stack-gap', $value); + } + + $index: $index + 1; + } +} diff --git a/yarn.lock b/yarn.lock index 55371bb4609c..2fa574ef04be 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10311,6 +10311,7 @@ __metadata: "@babel/runtime": ^7.14.6 "@carbon/feature-flags": ^0.6.0 "@carbon/icons-react": ^10.41.0 + "@carbon/layout": ^10.33.0 "@carbon/telemetry": 0.0.0-alpha.6 "@carbon/test-utils": ^10.19.0 "@cypress/react": ^5.4.0