Skip to content

Commit

Permalink
feat(react): add unstable_Stack component (#9876)
Browse files Browse the repository at this point in the history
* feat(react): add unstable_Stack component

* refactor(stack): update api and add tests

* chore(react): update entrypoints with unstable_Stack

* Update packages/react/src/components/Stack/index.js

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
  • Loading branch information
joshblack and kodiakhq[bot] authored Oct 25, 2021
1 parent 4c044be commit 18ce43c
Show file tree
Hide file tree
Showing 16 changed files with 478 additions and 0 deletions.
3 changes: 3 additions & 0 deletions packages/carbon-react/__tests__/index-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ Array [
"FormItem",
"FormLabel",
"Grid",
"HStack",
"Header",
"HeaderContainer",
"HeaderGlobalAction",
Expand Down Expand Up @@ -134,6 +135,7 @@ Array [
"SkipToContent",
"Slider",
"SliderSkeleton",
"Stack",
"StructuredListBody",
"StructuredListCell",
"StructuredListHead",
Expand Down Expand Up @@ -197,6 +199,7 @@ Array [
"TooltipDefinition",
"TooltipIcon",
"UnorderedList",
"VStack",
"unstable_Heading",
"unstable_PageSelector",
"unstable_Pagination",
Expand Down
3 changes: 3 additions & 0 deletions packages/carbon-react/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
61 changes: 61 additions & 0 deletions packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -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],
},
}
`;
1 change: 1 addition & 0 deletions packages/react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
3 changes: 3 additions & 0 deletions packages/react/src/__tests__/index-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,7 @@ Array [
"TooltipIcon",
"UnorderedList",
"unstable_FeatureFlags",
"unstable_HStack",
"unstable_Heading",
"unstable_Menu",
"unstable_MenuDivider",
Expand All @@ -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",
Expand Down
100 changes: 100 additions & 0 deletions packages/react/src/components/Stack/Stack.js
Original file line number Diff line number Diff line change
@@ -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<number>}
*/
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 (
<BaseComponent {...rest} ref={ref} className={className} style={style}>
{children}
</BaseComponent>
);
});

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 };
74 changes: 74 additions & 0 deletions packages/react/src/components/Stack/__tests__/Stack-test.e2e.js
Original file line number Diff line number Diff line change
@@ -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(
<PrefixContext.Provider value="cds">
{SPACING_STEPS.map((step) => {
return (
<Stack key={step} gap={step}>
<div>item 1</div>
<div>item 2</div>
<div>item 3</div>
</Stack>
);
})}
</PrefixContext.Provider>
);

cy.percySnapshot();
});

it('should support a horizontal orientation', () => {
mount(
<PrefixContext.Provider value="cds">
{SPACING_STEPS.map((step) => {
return (
<div key={step}>
<Stack gap={step} orientation="horizontal">
<div>item 1</div>
<div>item 2</div>
<div>item 3</div>
</Stack>
</div>
);
})}
</PrefixContext.Provider>
);

cy.percySnapshot();
});

it('should support a custom gap with the `gap` prop', () => {
mount(
<PrefixContext.Provider value="cds">
<Stack gap="20px">
<div>item 1</div>
<div>item 2</div>
<div>item 3</div>
</Stack>
</PrefixContext.Provider>
);

cy.percySnapshot();
});
});
88 changes: 88 additions & 0 deletions packages/react/src/components/Stack/__tests__/Stack-test.js
Original file line number Diff line number Diff line change
@@ -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(
<Stack as="section">
<article>one</article>
<article>two</article>
<article>three</article>
</Stack>
);

expect(container.firstChild.tagName).toBe('SECTION');
});

it('should support a custom className with the `className` prop', () => {
const { container } = render(
<Stack className="test">
<article>one</article>
<article>two</article>
<article>three</article>
</Stack>
);

expect(container.firstChild).toHaveClass('test');
});

it('should apply additional props to the outermost element', () => {
const { container } = render(
<Stack data-testid="test">
<article>one</article>
<article>two</article>
<article>three</article>
</Stack>
);

expect(container.firstChild).toHaveAttribute('data-testid', 'test');
});

it('should forward the given ref to the outermost element', () => {
const ref = jest.fn();
const { container } = render(
<Stack ref={ref}>
<article>one</article>
<article>two</article>
<article>three</article>
</Stack>
);
expect(ref).toHaveBeenCalledWith(container.firstChild);
});

describe('HStack', () => {
it('should forward the given ref to the outermost element', () => {
const ref = jest.fn();
const { container } = render(
<HStack ref={ref}>
<article>one</article>
<article>two</article>
<article>three</article>
</HStack>
);
expect(ref).toHaveBeenCalledWith(container.firstChild);
});
});

describe('VStack', () => {
it('should forward the given ref to the outermost element', () => {
const ref = jest.fn();
const { container } = render(
<VStack ref={ref}>
<article>one</article>
<article>two</article>
<article>three</article>
</VStack>
);
expect(ref).toHaveBeenCalledWith(container.firstChild);
});
});
});
Loading

0 comments on commit 18ce43c

Please sign in to comment.