Skip to content

Commit 18ce43c

Browse files
feat(react): add unstable_Stack component (#9876)
* 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>
1 parent 4c044be commit 18ce43c

16 files changed

Lines changed: 478 additions & 0 deletions

File tree

packages/carbon-react/__tests__/index-test.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ Array [
6262
"FormItem",
6363
"FormLabel",
6464
"Grid",
65+
"HStack",
6566
"Header",
6667
"HeaderContainer",
6768
"HeaderGlobalAction",
@@ -134,6 +135,7 @@ Array [
134135
"SkipToContent",
135136
"Slider",
136137
"SliderSkeleton",
138+
"Stack",
137139
"StructuredListBody",
138140
"StructuredListCell",
139141
"StructuredListHead",
@@ -197,6 +199,7 @@ Array [
197199
"TooltipDefinition",
198200
"TooltipIcon",
199201
"UnorderedList",
202+
"VStack",
200203
"unstable_Heading",
201204
"unstable_PageSelector",
202205
"unstable_Pagination",

packages/carbon-react/src/index.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,9 @@ export {
196196
unstable_useContextMenu,
197197
unstable_Heading,
198198
unstable_Section,
199+
unstable_HStack as HStack,
200+
unstable_Stack as Stack,
201+
unstable_VStack as VStack,
199202
} from 'carbon-components-react';
200203

201204
export {

packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8938,5 +8938,66 @@ Map {
89388938
},
89398939
},
89408940
"unstable_usePrefix" => Object {},
8941+
"unstable_HStack" => Object {
8942+
"$$typeof": Symbol(react.forward_ref),
8943+
"render": [Function],
8944+
},
8945+
"unstable_Stack" => Object {
8946+
"$$typeof": Symbol(react.forward_ref),
8947+
"propTypes": Object {
8948+
"as": Object {
8949+
"type": "elementType",
8950+
},
8951+
"children": Object {
8952+
"type": "node",
8953+
},
8954+
"className": Object {
8955+
"type": "string",
8956+
},
8957+
"gap": Object {
8958+
"args": Array [
8959+
Array [
8960+
Object {
8961+
"type": "string",
8962+
},
8963+
Object {
8964+
"args": Array [
8965+
Array [
8966+
1,
8967+
2,
8968+
3,
8969+
4,
8970+
5,
8971+
6,
8972+
7,
8973+
8,
8974+
9,
8975+
10,
8976+
11,
8977+
12,
8978+
],
8979+
],
8980+
"type": "oneOf",
8981+
},
8982+
],
8983+
],
8984+
"type": "oneOfType",
8985+
},
8986+
"orientation": Object {
8987+
"args": Array [
8988+
Array [
8989+
"horizontal",
8990+
"vertical",
8991+
],
8992+
],
8993+
"type": "oneOf",
8994+
},
8995+
},
8996+
"render": [Function],
8997+
},
8998+
"unstable_VStack" => Object {
8999+
"$$typeof": Symbol(react.forward_ref),
9000+
"render": [Function],
9001+
},
89419002
}
89429003
`;

packages/react/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
"@babel/runtime": "^7.14.6",
5050
"@carbon/feature-flags": "^0.6.0",
5151
"@carbon/icons-react": "^10.41.0",
52+
"@carbon/layout": "^10.33.0",
5253
"@carbon/telemetry": "0.0.0-alpha.6",
5354
"classnames": "2.3.1",
5455
"copy-to-clipboard": "^3.3.1",

packages/react/src/__tests__/index-test.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,7 @@ Array [
200200
"TooltipIcon",
201201
"UnorderedList",
202202
"unstable_FeatureFlags",
203+
"unstable_HStack",
203204
"unstable_Heading",
204205
"unstable_Menu",
205206
"unstable_MenuDivider",
@@ -211,8 +212,10 @@ Array [
211212
"unstable_Pagination",
212213
"unstable_ProgressBar",
213214
"unstable_Section",
215+
"unstable_Stack",
214216
"unstable_TreeNode",
215217
"unstable_TreeView",
218+
"unstable_VStack",
216219
"unstable_useContextMenu",
217220
"unstable_useFeatureFlag",
218221
"unstable_useFeatureFlags",
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
/**
2+
* Copyright IBM Corp. 2016, 2018
3+
*
4+
* This source code is licensed under the Apache-2.0 license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
import { spacing } from '@carbon/layout';
9+
import cx from 'classnames';
10+
import PropTypes from 'prop-types';
11+
import React from 'react';
12+
import { usePrefix } from '../../internal/usePrefix';
13+
14+
/**
15+
* The steps in the spacing scale
16+
* @type {Array<number>}
17+
*/
18+
const SPACING_STEPS = Array.from({ length: spacing.length - 1 }).map(
19+
(_, step) => {
20+
return step + 1;
21+
}
22+
);
23+
24+
/**
25+
* The Stack component is a useful layout utility in a component-based model.
26+
* This allows components to not use margin and instead delegate the
27+
* responsibility of positioning and layout to parent components.
28+
*
29+
* In the case of the Stack component, it uses the spacing scale from the
30+
* Design Language in order to determine how much space there should be between
31+
* items rendered by the Stack component. It also supports a custom `gap` prop
32+
* which will allow a user to provide a custom value for the gap of the layout.
33+
*
34+
* This component supports both horizontal and vertical orientations.
35+
*
36+
* Inspiration for this component:
37+
*
38+
* - https://paste.twilio.design/layout/stack/
39+
* - https://github.com/Workday/canvas-kit/blob/f2f599654876700f483a1d8c5de82a41315c76f1/modules/labs-react/layout/lib/Stack.tsx
40+
*/
41+
const Stack = React.forwardRef(function Stack(props, ref) {
42+
const {
43+
as: BaseComponent = 'div',
44+
children,
45+
className: customClassName,
46+
gap,
47+
orientation = 'vertical',
48+
...rest
49+
} = props;
50+
const prefix = usePrefix();
51+
const className = cx(customClassName, {
52+
[`${prefix}--stack-${orientation}`]: true,
53+
[`${prefix}--stack-scale-${gap}`]: typeof gap === 'number',
54+
});
55+
const style = {};
56+
57+
if (typeof gap === 'string') {
58+
style[`--${prefix}-stack-gap`] = gap;
59+
}
60+
61+
return (
62+
<BaseComponent {...rest} ref={ref} className={className} style={style}>
63+
{children}
64+
</BaseComponent>
65+
);
66+
});
67+
68+
Stack.propTypes = {
69+
/**
70+
* Provide a custom element type to render as the outermost element in
71+
* the Stack component. By default, this component will render a `div`.
72+
*/
73+
as: PropTypes.elementType,
74+
75+
/**
76+
* Provide the elements that will be rendered as children inside of the Stack
77+
* component. These elements will have having spacing between them according
78+
* to the `step` and `orientation` prop
79+
*/
80+
children: PropTypes.node,
81+
82+
/**
83+
* Provide a custom class name to be used by the outermost element rendered by
84+
* Stack
85+
*/
86+
className: PropTypes.string,
87+
88+
/**
89+
* Provide either a custom value or a step from the spacing scale to be used
90+
* as the gap in the layout
91+
*/
92+
gap: PropTypes.oneOfType([PropTypes.string, PropTypes.oneOf(SPACING_STEPS)]),
93+
94+
/**
95+
* Specify the orientation of them items in the Stack
96+
*/
97+
orientation: PropTypes.oneOf(['horizontal', 'vertical']),
98+
};
99+
100+
export { Stack };
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/**
2+
* Copyright IBM Corp. 2016, 2018
3+
*
4+
* This source code is licensed under the Apache-2.0 license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
import '@carbon/styles/scss/components/stack/_index.scss';
9+
10+
import { mount } from '@cypress/react';
11+
import { spacing } from '@carbon/layout';
12+
import React from 'react';
13+
import { Stack } from '../../Stack';
14+
import { PrefixContext } from '../../../internal/usePrefix';
15+
16+
const SPACING_STEPS = Array.from({ length: spacing.length - 1 }).map(
17+
(_, step) => {
18+
return step + 1;
19+
}
20+
);
21+
22+
describe('Stack', () => {
23+
it('should default to the vertical orientation', () => {
24+
mount(
25+
<PrefixContext.Provider value="cds">
26+
{SPACING_STEPS.map((step) => {
27+
return (
28+
<Stack key={step} gap={step}>
29+
<div>item 1</div>
30+
<div>item 2</div>
31+
<div>item 3</div>
32+
</Stack>
33+
);
34+
})}
35+
</PrefixContext.Provider>
36+
);
37+
38+
cy.percySnapshot();
39+
});
40+
41+
it('should support a horizontal orientation', () => {
42+
mount(
43+
<PrefixContext.Provider value="cds">
44+
{SPACING_STEPS.map((step) => {
45+
return (
46+
<div key={step}>
47+
<Stack gap={step} orientation="horizontal">
48+
<div>item 1</div>
49+
<div>item 2</div>
50+
<div>item 3</div>
51+
</Stack>
52+
</div>
53+
);
54+
})}
55+
</PrefixContext.Provider>
56+
);
57+
58+
cy.percySnapshot();
59+
});
60+
61+
it('should support a custom gap with the `gap` prop', () => {
62+
mount(
63+
<PrefixContext.Provider value="cds">
64+
<Stack gap="20px">
65+
<div>item 1</div>
66+
<div>item 2</div>
67+
<div>item 3</div>
68+
</Stack>
69+
</PrefixContext.Provider>
70+
);
71+
72+
cy.percySnapshot();
73+
});
74+
});
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/**
2+
* Copyright IBM Corp. 2016, 2018
3+
*
4+
* This source code is licensed under the Apache-2.0 license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
import { render } from '@testing-library/react';
9+
import React from 'react';
10+
import { HStack, Stack, VStack } from '../../Stack';
11+
12+
describe('Stack', () => {
13+
it('should support alternate element types with the `as` prop', () => {
14+
const { container } = render(
15+
<Stack as="section">
16+
<article>one</article>
17+
<article>two</article>
18+
<article>three</article>
19+
</Stack>
20+
);
21+
22+
expect(container.firstChild.tagName).toBe('SECTION');
23+
});
24+
25+
it('should support a custom className with the `className` prop', () => {
26+
const { container } = render(
27+
<Stack className="test">
28+
<article>one</article>
29+
<article>two</article>
30+
<article>three</article>
31+
</Stack>
32+
);
33+
34+
expect(container.firstChild).toHaveClass('test');
35+
});
36+
37+
it('should apply additional props to the outermost element', () => {
38+
const { container } = render(
39+
<Stack data-testid="test">
40+
<article>one</article>
41+
<article>two</article>
42+
<article>three</article>
43+
</Stack>
44+
);
45+
46+
expect(container.firstChild).toHaveAttribute('data-testid', 'test');
47+
});
48+
49+
it('should forward the given ref to the outermost element', () => {
50+
const ref = jest.fn();
51+
const { container } = render(
52+
<Stack ref={ref}>
53+
<article>one</article>
54+
<article>two</article>
55+
<article>three</article>
56+
</Stack>
57+
);
58+
expect(ref).toHaveBeenCalledWith(container.firstChild);
59+
});
60+
61+
describe('HStack', () => {
62+
it('should forward the given ref to the outermost element', () => {
63+
const ref = jest.fn();
64+
const { container } = render(
65+
<HStack ref={ref}>
66+
<article>one</article>
67+
<article>two</article>
68+
<article>three</article>
69+
</HStack>
70+
);
71+
expect(ref).toHaveBeenCalledWith(container.firstChild);
72+
});
73+
});
74+
75+
describe('VStack', () => {
76+
it('should forward the given ref to the outermost element', () => {
77+
const ref = jest.fn();
78+
const { container } = render(
79+
<VStack ref={ref}>
80+
<article>one</article>
81+
<article>two</article>
82+
<article>three</article>
83+
</VStack>
84+
);
85+
expect(ref).toHaveBeenCalledWith(container.firstChild);
86+
});
87+
});
88+
});

0 commit comments

Comments
 (0)