-
Notifications
You must be signed in to change notification settings - Fork 302
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Convert balstack to a render function component #1313
Changes from 7 commits
ec96bd0
bec5fbc
4d01295
de554a7
1da9e9d
5292edd
576de39
bcd1645
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
import { render } from '@testing-library/vue'; | ||
import BalStack from './BalStack.vue'; | ||
|
||
describe.only('BalStack', () => { | ||
describe('When using BalStack', () => { | ||
it('should render items', () => { | ||
const { getByText } = render(BalStack, { | ||
slots: { | ||
default: '<div>First</div><div>Second</div><div>Third</div>' | ||
} | ||
}); | ||
|
||
// check that elements are actually rendered as children | ||
expect(getByText('First')).toBeVisible(); | ||
expect(getByText('Second')).toBeVisible(); | ||
expect(getByText('Third')).toBeVisible(); | ||
}); | ||
|
||
it('should render items horizontally when the horizontal prop is supplied', () => { | ||
const { getByText } = render(BalStack, { | ||
slots: { | ||
default: '<div>First</div><div>Second</div><div>Third</div>' | ||
}, | ||
props: { | ||
horizontal: true | ||
} | ||
}); | ||
|
||
// its fine to make this assumption here as we render the children without any wrappers | ||
const stackEl = getByText('First').parentElement; | ||
expect(stackEl).toHaveClass('flex-row'); | ||
}); | ||
|
||
it('should render items verticlly if vertical prop is supplied', () => { | ||
const { getByText } = render(BalStack, { | ||
slots: { | ||
default: '<div>First</div><div>Second</div><div>Third</div>' | ||
}, | ||
props: { | ||
vertical: true | ||
} | ||
}); | ||
|
||
// its fine to make this assumption here as we render the children without any wrappers | ||
const stackEl = getByText('First').parentElement; | ||
expect(stackEl).toHaveClass('flex-col'); | ||
}); | ||
|
||
it('should render items with space between them', () => { | ||
const { getByText } = render(BalStack, { | ||
slots: { | ||
default: '<div>First</div><div>Second</div><div>Third</div>' | ||
}, | ||
props: { | ||
vertical: true | ||
} | ||
}); | ||
|
||
// the default spacing unit (tailwind) is 4. So can be either mb-4 or mr-4 | ||
expect(getByText('First')).toHaveClass('mb-4'); | ||
expect(getByText('Second')).toHaveClass('mb-4'); | ||
// last el shouldn't have a spacing class | ||
expect(getByText('Third')).not.toHaveClass('mb-4'); | ||
}); | ||
it('should render items with a border between them if withBorder prop is supplied', () => { | ||
const { getByText } = render(BalStack, { | ||
slots: { | ||
default: '<div>First</div><div>Second</div><div>Third</div>' | ||
}, | ||
props: { | ||
vertical: true, | ||
withBorder: true | ||
} | ||
}); | ||
|
||
// the default spacing unit (tailwind) is 4. So can be either mb-4 or mr-4 | ||
expect(getByText('First')).toHaveClass('mb-4 border-b'); | ||
expect(getByText('Second')).toHaveClass('mb-4 border-b'); | ||
// last el shouldn't have a spacing class | ||
expect(getByText('Third')).not.toHaveClass('mb-4 border-b'); | ||
Comment on lines
+77
to
+80
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These tests are great! Just a minor thing here, is there any advantage to maybe using the divide class and flexgrid rather than margins and flexbox? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'll look into it |
||
}); | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -1,20 +1,12 @@ | ||||||
<script setup lang="ts"> | ||||||
import { uniqueId } from 'lodash'; | ||||||
import { computed, useSlots } from 'vue'; | ||||||
<script lang="ts"> | ||||||
import { defineComponent, PropType, h } from 'vue'; | ||||||
|
||||||
type Spacing = 'xs' | 'sm' | 'base' | 'lg' | 'xl' | '2xl' | 'none'; | ||||||
type Props = { | ||||||
vertical?: boolean; | ||||||
horizontal?: boolean; | ||||||
spacing?: Spacing; | ||||||
withBorder?: boolean; | ||||||
ref?: any; | ||||||
align?: 'center' | 'start' | 'end' | 'between'; | ||||||
justify?: 'center' | 'start' | 'end' | 'between'; | ||||||
isDynamic?: boolean; | ||||||
expandChildren?: boolean; | ||||||
}; | ||||||
type Alignment = 'center' | 'start' | 'end' | 'between'; | ||||||
|
||||||
/** | ||||||
* Maps discrete spacing types to a tailwind spacing tier | ||||||
*/ | ||||||
const SpacingMap: Record<Spacing, number> = { | ||||||
xs: 1, | ||||||
sm: 2, | ||||||
|
@@ -25,63 +17,124 @@ const SpacingMap: Record<Spacing, number> = { | |||||
none: 0 | ||||||
}; | ||||||
|
||||||
const props = withDefaults(defineProps<Props>(), { | ||||||
spacing: 'base' | ||||||
}); | ||||||
|
||||||
const spacingClass = computed(() => { | ||||||
const spacingType = props.vertical ? 'mb' : 'mr'; | ||||||
return `${spacingType}-${SpacingMap[props.spacing]}`; | ||||||
}); | ||||||
|
||||||
const stackId = uniqueId(); | ||||||
const slots = useSlots(); | ||||||
|
||||||
const slotsWithContent = computed(() => { | ||||||
if (props.isDynamic) { | ||||||
if (Array.isArray(slots.default()[0].children)) { | ||||||
return (slots.default()[0].children as any[]).filter( | ||||||
child => child.children !== 'v-if' | ||||||
); | ||||||
export default defineComponent({ | ||||||
props: { | ||||||
/** | ||||||
* Stacked top down | ||||||
*/ | ||||||
vertical: { type: Boolean, default: () => false }, | ||||||
/** | ||||||
* Stacked left to right | ||||||
*/ | ||||||
horizontal: { type: Boolean, default: () => false }, | ||||||
spacing: { | ||||||
type: String as PropType<Spacing>, | ||||||
default: () => 'base' | ||||||
}, | ||||||
/** | ||||||
* Show a hairline border after each stack element | ||||||
*/ | ||||||
withBorder: { | ||||||
type: Boolean, | ||||||
default: () => false | ||||||
}, | ||||||
/** | ||||||
* Flex align prop | ||||||
*/ | ||||||
align: { | ||||||
type: String as PropType<Alignment> | ||||||
}, | ||||||
/** | ||||||
* Flex justify prop | ||||||
*/ | ||||||
justify: { | ||||||
type: String as PropType<Alignment> | ||||||
}, | ||||||
/** | ||||||
* Will cause children of the stack to occupy | ||||||
* as much space as possible. | ||||||
*/ | ||||||
expandChildren: { | ||||||
type: Boolean, | ||||||
default: () => false | ||||||
} | ||||||
}, | ||||||
setup(props, { slots, attrs }) { | ||||||
return { | ||||||
slotsWithContent: [], | ||||||
slots, | ||||||
attrs | ||||||
}; | ||||||
}, | ||||||
render() { | ||||||
const spacingType = this.vertical ? 'mb' : 'mr'; | ||||||
const borderType = this.vertical ? 'b' : 'r'; | ||||||
const widthClass = this.expandChildren ? 'w-full' : ''; | ||||||
const borderClass = this.withBorder ? `border-${borderType}` : ''; | ||||||
const stackNodeClass = `dark:border-gray-600 ${spacingType}-${ | ||||||
SpacingMap[this.spacing] | ||||||
} ${borderClass} ${widthClass}`; | ||||||
|
||||||
// @ts-ignore | ||||||
const vNodes = this.$slots.default() || []; | ||||||
// if a childs 'value' is 'v-if', it is not visible so filter it out | ||||||
// to not cause an empty node to render with margin | ||||||
const children = vNodes.filter(vNode => vNode.children !== 'v-if'); | ||||||
// need to apply margin and decorator classes to all children | ||||||
const styledChildren = children.map((child, childIndex) => { | ||||||
let styledNestedChildren = child.children; | ||||||
// a child can have an array of nested children, this is when | ||||||
// those children are rendered as part of a 'v-for directive' | ||||||
if (Array.isArray(styledNestedChildren)) { | ||||||
// and those children can be nullish too | ||||||
const nonNullishChildren = styledNestedChildren.filter( | ||||||
nestedChild => nestedChild !== undefined || nestedChild !== null | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I assume you wanted
Suggested change
|
||||||
); | ||||||
styledNestedChildren = nonNullishChildren.map( | ||||||
(nestedChild, nestedChildIndex) => { | ||||||
//@ts-ignore | ||||||
return h(nestedChild, { | ||||||
class: | ||||||
nestedChildIndex !== nonNullishChildren.length - 1 | ||||||
? stackNodeClass | ||||||
: null | ||||||
}); | ||||||
} | ||||||
); | ||||||
return h( | ||||||
child, | ||||||
{ | ||||||
class: childIndex !== children.length - 1 ? stackNodeClass : null | ||||||
}, | ||||||
[styledNestedChildren] | ||||||
); | ||||||
} | ||||||
return h(child, { | ||||||
class: childIndex !== children.length - 1 ? stackNodeClass : null | ||||||
}); | ||||||
}); | ||||||
return h( | ||||||
'div', | ||||||
{ | ||||||
attrs: this.$attrs, | ||||||
class: [ | ||||||
'flex', | ||||||
{ | ||||||
'flex-row': this.horizontal, | ||||||
'flex-col': this.vertical, | ||||||
'items-center': this.align === 'center', | ||||||
'items-start': this.align === 'start', | ||||||
'items-end': this.align === 'end', | ||||||
'items-between': this.align === 'between', | ||||||
'justify-center': this.justify === 'center', | ||||||
'justify-start': this.justify === 'start', | ||||||
'justify-end': this.justify === 'end', | ||||||
'justify-between': this.justify === 'between' | ||||||
} | ||||||
] | ||||||
}, | ||||||
[styledChildren] | ||||||
); | ||||||
} | ||||||
return slots.default().filter(slot => { | ||||||
if (slot.children !== 'v-if') return true; | ||||||
return false; | ||||||
}); | ||||||
}); | ||||||
</script> | ||||||
|
||||||
<template> | ||||||
<div | ||||||
:class="[ | ||||||
'flex', | ||||||
{ | ||||||
'flex-row': horizontal, | ||||||
'flex-col': vertical, | ||||||
'items-center': align === 'center', | ||||||
'items-start': align === 'start', | ||||||
'items-end': align === 'end', | ||||||
'items-between': align === 'between', | ||||||
'justify-center': justify === 'center', | ||||||
'justify-start': justify === 'start', | ||||||
'justify-end': justify === 'end', | ||||||
'justify-between': justify === 'between' | ||||||
} | ||||||
]" | ||||||
> | ||||||
<component | ||||||
v-for="(child, i) in slotsWithContent" | ||||||
:key="`stack-${stackId}-child-${i}-${child?.key || ''}`" | ||||||
:is="child" | ||||||
:class="{ | ||||||
[spacingClass]: i !== slotsWithContent.length - 1, | ||||||
'border-b': i !== slotsWithContent.length - 1 && withBorder && vertical, | ||||||
'border-r': | ||||||
i !== slotsWithContent.length - 1 && withBorder && horizontal, | ||||||
'w-full': expandChildren, | ||||||
'dark:border-gray-600': true | ||||||
}" | ||||||
/> | ||||||
</div> | ||||||
</template> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks nice! Glad we have something to do UI tests!