Skip to content

Commit

Permalink
fix(Flex.Container): add Flex.withChildren HOC for handling wrapped…
Browse files Browse the repository at this point in the history
… children with spacing (#3200)
  • Loading branch information
tujoworker authored Jan 10, 2024
1 parent fe0dbef commit 93df77c
Show file tree
Hide file tree
Showing 11 changed files with 273 additions and 15 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -193,3 +193,29 @@ export const LayoutHorizontalFlexGrowItems = () => {
</ComponentBox>
)
}

export const WrappedWithChildren = () => {
return (
<ComponentBox
scope={{ TestElement }}
data-visual-test="flex-container-with-children"
>
{() => {
const Wrapper = Flex.withChildren(({ children }) => {
return <div>{children}</div>
})

return (
<Flex.Container direction="vertical">
<TestElement>FlexItem 1</TestElement>
<Wrapper>
<TestElement>FlexItem 2</TestElement>
<TestElement>FlexItem 3</TestElement>
</Wrapper>
<TestElement>FlexItem 4</TestElement>
</Flex.Container>
)
}}
</ComponentBox>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,7 @@ Will wrap on small screens.
### Vertical aligned Field.String

<Examples.VerticalWithFieldString />

### Flex.withChildren

<Examples.WrappedWithChildren />
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,27 @@ You may else wrap your custom component in a `Flex.Item` – this way, you still

Technically, `Flex.Container` checks if a nested component has a property called `_supportsSpacingProps`. So if you have a component that supports the [spacing properties](/uilib/layout/space/), you can add this property `ComponentName._supportsSpacingProps = true`.

If the component is a wrapper component, and you want its children to support spacing, you can add this property `ComponentName._supportsSpacingProps = 'children'`.

But for simplicity, you can use the HOC `Flex.withChildren`:

```tsx
const Wrapper = Flex.withChildren(({ children }) => {
return <div>{children}</div>
})

render(
<Flex.Container direction="vertical">
<Item />
<Wrapper>
<Item />
<Item />
</Wrapper>
<Item />
</Flex.Container>,
)
```

### Horizontal and Vertical aliases

For shortening the usage of `direction="..."`, you can use:
Expand Down
19 changes: 18 additions & 1 deletion packages/dnb-eufemia/src/components/flex/Container.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ function FlexContainer(props: Props) {
...rest
} = props

const childrenArray = React.Children.toArray(children)
const childrenArray = wrapChildren(props, children)
const hasHeading = childrenArray.some((child, i) => {
const previousChild = childrenArray?.[i - 1]
return (
Expand Down Expand Up @@ -204,6 +204,23 @@ function FlexContainer(props: Props) {
)
}

function wrapChildren(props: Props, children: React.ReactNode) {
return React.Children.toArray(children).map((child) => {
if (
React.isValidElement(child) &&
child.type['_supportsSpacingProps'] === 'children'
) {
return React.cloneElement(
child,
child.props,
<FlexContainer {...props}>{child.props.children}</FlexContainer>
)
}

return child
})
}

FlexContainer._supportsSpacingProps = true

export default FlexContainer
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,15 @@ describe('Flex.Container', () => {
expect(screenshot).toMatchImageSnapshot()
})

it('have to match with children', async () => {
const screenshot = await makeScreenshot({
url: '/uilib/layout/flex/container/demos',
selector:
'[data-visual-test="flex-container-with-children"] .dnb-flex-container',
})
expect(screenshot).toMatchImageSnapshot()
})

it('have to match field on large viewport', async () => {
const screenshot = await makeScreenshot({
url: '/uilib/layout/flex/container/demos',
Expand Down
164 changes: 150 additions & 14 deletions packages/dnb-eufemia/src/components/flex/__tests__/Container.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -346,21 +346,8 @@ describe('Flex.Container', () => {
expect(children[2].className).toContain('dnb-space__right--zero')
})

it('should set element', () => {
render(<Flex.Container element="section">content</Flex.Container>)

const element = document.querySelector('.dnb-flex-container')

expect(element.tagName).toBe('SECTION')
})

it('should not add a wrapper when _supportsSpacingProps is given', () => {
const { rerender } = render(
<Flex.Vertical>
<Flex.Item>content</Flex.Item>
<Flex.Item>content</Flex.Item>
</Flex.Vertical>
)
const { rerender } = render(<></>)

const TestComponent = (props: SpaceProps) => {
const cn = createSpacingClasses(props)
Expand Down Expand Up @@ -417,6 +404,155 @@ describe('Flex.Container', () => {
}
})

it('should transform children if _supportsSpacingProps="children" is given', () => {
const { rerender } = render(<></>)

const Wrapper = ({ children }) => {
return <div className="wrapper">{children}</div>
}

const TestComponent = (props: SpaceProps) => {
const cn = createSpacingClasses(props)
cn.push('test-item')
return <div className={cn.join(' ')}>content</div>
}

{
rerender(
<Flex.Vertical>
<Wrapper>
<TestComponent />
<TestComponent top="large" />
</Wrapper>
</Flex.Vertical>
)

const elements = document.querySelectorAll(
'.dnb-flex-container > div'
)
expect(elements).toHaveLength(1)
expect(elements[0].className).toBe(
'dnb-space dnb-space__top--zero dnb-space__bottom--zero'
)
expect((elements[0].firstChild as HTMLElement).className).toBe(
'wrapper'
)
}

{
Wrapper._supportsSpacingProps = 'children'

rerender(
<Flex.Vertical>
<Wrapper>
<TestComponent />
<TestComponent top="large" />
</Wrapper>
</Flex.Vertical>
)

{
const elements = Array.from(
document.querySelectorAll('.dnb-flex-container > div')
)

expect(elements).toHaveLength(3)
expect(elements[0]).toHaveClass(
'dnb-space dnb-space__top--zero dnb-space__bottom--zero'
)
expect(elements[1]).toHaveClass(
'dnb-space dnb-space__top--zero dnb-space__bottom--zero'
)
expect(elements[2]).toHaveClass(
'dnb-space dnb-space__top--large dnb-space__bottom--zero'
)
}

{
const elements = Array.from(
document.querySelectorAll('.dnb-flex-container > div > div')
)

expect(elements).toHaveLength(3)
expect(elements[0]).toHaveClass('wrapper')
expect(elements[1]).toHaveClass('test-item')
expect(elements[2]).toHaveClass('test-item')
}

{
const elements = Array.from(
document.querySelectorAll('.dnb-flex-container')
)

expect(elements).toHaveLength(2)
}

{
const elements = Array.from(
document.querySelectorAll(
'body > div > .dnb-flex-container > div'
)
)

expect(elements).toHaveLength(1)
expect(elements[0]).toHaveClass(
'dnb-space dnb-space__top--zero dnb-space__bottom--zero'
)
}
}

{
TestComponent._supportsSpacingProps = true

rerender(
<Flex.Vertical>
<Wrapper>
<TestComponent />
<TestComponent top="x-large" />
</Wrapper>
</Flex.Vertical>
)

{
const elements = Array.from(
document.querySelectorAll(
'body > div > .dnb-flex-container > div'
)
)

expect(elements).toHaveLength(1)
expect(elements[0]).toHaveClass(
'dnb-space dnb-space__top--zero dnb-space__bottom--zero'
)
}

{
const elements = Array.from(
document.querySelectorAll('.dnb-flex-container > div')
)

expect(elements).toHaveLength(3)
expect(elements[0]).toHaveClass(
'dnb-space dnb-space__top--zero dnb-space__bottom--zero'
)
expect(elements[1]).toHaveClass(
'dnb-space__top--zero dnb-space__bottom--zero test-item'
)
expect(elements[2]).toHaveClass(
'dnb-space__top--x-large dnb-space__bottom--zero test-item'
)
}
}
})

it('should set custom element', () => {
render(<Flex.Container element="section">content</Flex.Container>)

const element = document.querySelector('.dnb-flex-container')

expect(element.tagName).toBe('SECTION')
})

it('gets valid ref element', () => {
let ref: React.RefObject<HTMLInputElement>

Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions packages/dnb-eufemia/src/components/flex/export.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ export { default as Item } from './Item'
export { default as Stack } from './Stack'
export { default as Horizontal } from './Horizontal'
export { default as Vertical } from './Vertical'
export { default as withChildren } from './withChildren'
28 changes: 28 additions & 0 deletions packages/dnb-eufemia/src/components/flex/stories/Flex.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/**
* @dnb/eufemia Component Story
*
*/

import { TestElement } from '../../../extensions/forms'
import Flex from '../Flex'

export default {
title: 'Eufemia/Components/Flex',
}

const Wrapper = Flex.withChildren(({ children }) => {
return <div className="wrapper">{children}</div>
})

export function FlexWithChildren() {
return (
<Flex.Container direction="vertical">
<TestElement>FlexItem 1</TestElement>
<Wrapper>
<TestElement>FlexItem 2</TestElement>
<TestElement>FlexItem 3</TestElement>
</Wrapper>
<TestElement>FlexItem 4</TestElement>
</Flex.Container>
)
}
14 changes: 14 additions & 0 deletions packages/dnb-eufemia/src/components/flex/withChildren.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import React from 'react'

type WithChildrenProps = {
children?: React.ReactNode
}

function withChildren<T>(
Component: React.ComponentType<T>
): React.ComponentType<T & WithChildrenProps> {
Component['_supportsSpacingProps'] = 'children'
return Component
}

export default withChildren
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,5 @@ export default function TestElement({ className = null, ...props }) {
/>
)
}

TestElement._supportsSpacingProps = true

0 comments on commit 93df77c

Please sign in to comment.