Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/neat-items-sniff.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@stacks/ui': minor
---

This update improves the internal workings of the <Stack> component to no longer pass margin props to its children, but instead rely on css to space its children.
102 changes: 59 additions & 43 deletions packages/ui/src/stack.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { Children, cloneElement, isValidElement } from 'react';
import { ForwardRefExoticComponentWithAs, forwardRefWithAs } from '@stacks/ui-core';
import React, { useCallback } from 'react';
import { ForwardRefExoticComponentWithAs, forwardRefWithAs, get, Theme } from '@stacks/ui-core';

import { Flex, FlexProps } from './flex';
import { Box } from './box';
Expand All @@ -12,6 +12,20 @@ export interface StackProps extends FlexProps {
shouldWrapChildren?: boolean;
}

/**
* Gets only the valid children of a component,
* and ignores any nullish or falsy child.
*
* @param children the children
*/
export function getValidChildren(children: React.ReactNode) {
return React.Children.toArray(children).filter(child =>
React.isValidElement(child)
) as React.ReactElement[];
}

export const selector = '& > *:not(style):not(:last-child)';

export const Stack: ForwardRefExoticComponentWithAs<StackProps, 'div'> = forwardRefWithAs<
StackProps,
'div'
Expand All @@ -20,59 +34,61 @@ export const Stack: ForwardRefExoticComponentWithAs<StackProps, 'div'> = forward
isInline,
as,
children,
align,
justify,
alignItems,
justifyContent,
spacing = 'tight',
shouldWrapChildren,
divider,
divider = null,
...rest
} = props;
const validChildren = Array.isArray(children) ? children.filter(isValidElement) : [];
const validChildren = getValidChildren(children);

const spacingProps = useCallback(
(theme: Theme) => {
const value = get(theme, 'space')[spacing as string];
return isInline
? {
marginTop: 0,
marginInlineEnd: value,
marginBottom: 0,
marginInlineStart: 0,
}
: { marginBottom: value };
},
[spacing]
);
const cssStyles = useCallback((theme: Theme) => ({ [selector]: spacingProps(theme) }), [
spacingProps,
]);

return (
return validChildren?.length ? (
<Flex
align={align}
justify={justify}
alignItems={alignItems}
justifyContent={justifyContent}
flexDirection={isInline ? 'row' : 'column'}
as={as}
ref={ref}
css={cssStyles}
{...rest}
>
{validChildren?.length
? Children.map(validChildren, (child, index) => {
if (!isValidElement(child)) {
return null;
}
if (!Array.isArray(children)) {
return null;
}
const isLastChild = validChildren.length === index + 1;
const spacingProps = isInline
? { mr: isLastChild ? undefined : spacing }
: { mb: isLastChild ? undefined : spacing };

const Divider = divider ? cloneElement(divider, spacingProps) : null;

if (shouldWrapChildren) {
return (
<>
<Box display="inline-block" {...spacingProps}>
{child}
</Box>
{!isLastChild ? Divider : null}
</>
);
}
return (
<>
{cloneElement(child, spacingProps)}
{!isLastChild ? Divider : null}
</>
);
})
: children}
{validChildren.map((child, index) => {
const isLastChild = validChildren.length === index + 1;
if (shouldWrapChildren)
return (
<React.Fragment key={index}>
<Box display="inline-block">{child}</Box>
{!isLastChild ? divider : null}
</React.Fragment>
);
return (
<React.Fragment key={index}>
{child}
{!isLastChild && divider ? divider : null}
</React.Fragment>
);
})}
</Flex>
);
) : null;
});

Stack.displayName = 'Stack';
4 changes: 4 additions & 0 deletions packages/ui/src/utils/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React from 'react';
import { themeGet } from '@styled-system/theme-get';

let _window: Window | undefined = undefined;

Expand Down Expand Up @@ -378,3 +379,6 @@ export function mapResponsive(prop: any, mapper: (val: any) => any) {

export const startPad = (n: number, z = 2, s = '0') =>
(n + '').length <= z ? ['', '-'][+(n < 0)] + (s.repeat(z) + Math.abs(n)).slice(-1 * z) : n + '';

export const spacingGet = (path: string, fallback?: string): ((props: any) => any) =>
themeGet('spacing.' + path, fallback);