Skip to content

Commit

Permalink
feat: add labels prop and slot WIP
Browse files Browse the repository at this point in the history
See #1
  • Loading branch information
theetrain committed Jul 19, 2024
1 parent 29be2c1 commit 5919185
Show file tree
Hide file tree
Showing 9 changed files with 227 additions and 18 deletions.
4 changes: 2 additions & 2 deletions e2e/svelte-4/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 20 additions & 0 deletions e2e/svelte-4/src/routes/labels/+page.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<script>
import { Cartesian } from "svelte-cartesian"
import Button from "$lib/Button.svelte"
const props = {
props: {
size: ["small", "medium", "large"],
variant: ["primary", "secondary"],
},
Component: Button,
}
</script>

<Cartesian {...props} labels="short">Make popcorn</Cartesian>

<Cartesian {...props} asChild labels="long">
<Button>Make popcorn</Button>
</Cartesian>

<!-- TODO: test 'short' and 'true' instances, and all other kinds -->
8 changes: 7 additions & 1 deletion eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,11 @@ const compat = new FlatCompat()
export default [
// standard compatibility
...compat.extends('eslint-config-standard'),
...eslintPluginSvelte.configs['flat/recommended']
...eslintPluginSvelte.configs['flat/recommended'],
{
rules: {
'no-multi-str': 0,
'operator-linebreak': ['error', 'before']
}
}
]
1 change: 1 addition & 0 deletions jsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"sourceMap": true,
"strict": true,
"module": "ESNext",
"lib": ["ESNext"],
"moduleResolution": "Bundler"
},
"exclude": ["e2e/**"]
Expand Down
79 changes: 71 additions & 8 deletions lib/Cartesian.svelte
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script>
import { getCartesianProduct } from "./cartesian"
import { createLabel, getCartesianProduct } from "./cartesian"
/** A Svelte component. */
export let Component
Expand All @@ -18,6 +18,19 @@
*/
export let asChild = false
/**
* Generate labels under every iteration.
*
* - **true**: same as `'short'`.
* - **short**: display comma-separated values, skip objects.
* - **long**: display line-separated key-value pairs, represent object values
* as their type name.
* - **long-with-objects**: same as `'long'` but with full object definitions.
* @type {undefined | boolean | 'short' | 'long' | 'long-with-objects'}
* @default undefined
*/
export let labels = undefined
/**
* Disable built-in CSS.
* @type {boolean}
Expand All @@ -33,25 +46,75 @@
const cartesianProps = getCartesianProduct(props)
</script>

<div class:container={!unstyled} {...divAttributes}>
<!--
@component
A single component that helps render prop combinations
(the "Cartesian Product") for visual regression testing.
-->

<div class:sc-container={!unstyled} {...divAttributes}>
{#each cartesianProps as innerProps}
<div>
{@const label = labels && createLabel(innerProps, { verbosity: labels })}
<div class="sc-group">
{#if asChild}
<slot {innerProps} />
<div>
<slot {innerProps} />
</div>
<div>
<slot name="labels">
{#if labels}
<pre class="sc-label">{label}</pre>
{/if}
</slot>
</div>
{:else}
<svelte:component this={Component} {...innerProps}>
<slot />
</svelte:component>
<div>
<svelte:component this={Component} {...innerProps}>
<slot />
</svelte:component>
</div>
<div>
<slot name="labels">
{#if labels}
<pre class="sc-label">{label}</pre>
{/if}
</slot>
</div>
{/if}
</div>
{/each}
</div>

<style>
.container {
:where(.sc-container) {
display: grid;
grid-template-columns: var(--columns, repeat(2, 1fr));
gap: 1rem;
padding: 0.5rem 1rem;
}
:where(.sc-group) {
display: flex;
flex-direction: column;
}
:where(.sc-label) {
display: inline-block;
background-color: #fff;
color: #000;
padding: 0.25rem;
margin: 0.25rem;
border-radius: 3px;
font-family:
system-ui,
-apple-system,
BlinkMacSystemFont,
"Segoe UI",
Roboto,
Oxygen,
Ubuntu,
Cantarell,
"Open Sans",
"Helvetica Neue",
sans-serif;
font-size: var(--label-font-size, 0.875rem);
}
</style>
15 changes: 15 additions & 0 deletions lib/Cartesian.svelte.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,17 @@ interface Props {
* @default false
*/
asChild?: boolean
/**
* Generate labels under every iteration.
*
* - **true**: same as `'short'`.
* - **short**: display comma-separated values, skip objects.
* - **long**: display line-separated key-value pairs, represent object values
* as their type name.
* - **long-with-objects**: same as `'long'` but with full object definitions.
* @default undefined
*/
labels?: undefined | boolean | 'short' | 'long' | 'long-with-objects'
/**
* Disable built-in CSS.
* @default false
Expand All @@ -26,6 +37,10 @@ interface Props {
divAttributes?: RestProps
}

/**
* A single component that helps render prop combinations
* (the "Cartesian Product") for visual regression testing.
*/
export default class Cartesian extends SvelteComponent<
Props,
{},
Expand Down
58 changes: 53 additions & 5 deletions lib/cartesian.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
/**
* Convert props with arrays of values into their
* Cartesian Product: an array of prop combinations.
* @param {{[key: string]: any[]}} obj
* @returns {{ [key: string]: any }[]}
*/
* @typedef {{ [key: string]: any }} CartesianProp
*/

/**
* Convert props with arrays of values into their
* Cartesian Product: an array of prop combinations.
* @param {{[key: string]: any[]}} obj
* @returns {CartesianProp[]}
*/
export function getCartesianProduct (obj) {
const entries = Object.entries(obj)

Expand Down Expand Up @@ -31,3 +35,47 @@ export function getCartesianProduct (obj) {

return result
}

/**
* Creates a label to render for a given component combination.
* @param {CartesianProp} innerProps
* @param {{verbosity?: boolean | 'short' | 'long' | 'long-with-objects'}} [options={ verbosity: 'short' }]
*/
export function createLabel (
innerProps,
{ verbosity } = { verbosity: 'short' }
) {
const label = []
const joinCharacter = verbosity === 'short' ? ', ' : '\n'

for (const [key, value] of Object.entries(innerProps)) {
let refinedValue = value

if (
verbosity === 'short'
&& typeof value !== 'string'
&& typeof value !== 'number'
) {
// Skip symbols and objects for 'short' labels
continue
}

if (
verbosity === 'long'
&& typeof value !== 'string'
&& typeof value !== 'number'
) {
refinedValue = typeof value
} else if (verbosity === 'long-with-objects' && typeof value === 'object') {
refinedValue = JSON.stringify(value, null, 1)
}

if (verbosity === 'long' || verbosity === 'long-with-objects') {
label.push(`${key}: ${refinedValue}`)
} else if (verbosity === 'short') {
label.push(refinedValue)
}
}

return label.join(joinCharacter)
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
],
"scripts": {
"check": "svelte-check",
"test:unit": "vitest"
"test": "vitest"
},
"author": "Enrico Sacchetti <[email protected]>",
"license": "MIT",
Expand Down
58 changes: 57 additions & 1 deletion tests/cartesian.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest'
import { getCartesianProduct } from '../lib/cartesian'
import { createLabel, getCartesianProduct } from '../lib/cartesian'

describe('getCartesianProduct', () => {
it('returns prop combinations', () => {
Expand Down Expand Up @@ -38,3 +38,59 @@ describe('getCartesianProduct', () => {
])
})
})

describe('createLabel', () => {
it('returns short labels (default behaviour)', () => {
expect(createLabel({ variant: 'primary' }))
.toBe('primary')

// TODO: test 'true' and 'short' instances
expect(createLabel({ variant: 'primary' }, { verbosity: 'long' }))
.toBe('primary')

expect(createLabel({ variant: 'primary', size: 'md' }))
.toBe('primary, md')
})

it('returns long labels', () => {
expect(createLabel({ variant: 'primary' }, { verbosity: 'long' }))
.toBe('variant: primary')

expect(createLabel({
variant: 'primary',
size: 'md',
obj: { hello: 'world' }
}, { verbosity: 'long' }))
.toBe('variant: primary\nsize: md\nobj: object')
})

it('handles functions and symbols (short)', () => {
expect(createLabel({
variant: 'primary',
cb: (/** @type {Event} */ e) => {
e.preventDefault()
},
obj: { hello: 'world' },
sym: Symbol('foo')
}))
.toBe('primary')
})

it('returns object contents', () => {
expect(createLabel({
variant: 'primary',
cb: (/** @type {Event} */ e) => {
e.preventDefault()
},
obj: { hello: 'world' }
}, { verbosity: 'long-with-objects' }))
.toBe(
'variant: primary\n\
cb: (/** @type {Event} */ e) => {\n\
e.preventDefault()\n\
}\n\
obj: {\n\
"hello": "world"\n\
}')
})
})

0 comments on commit 5919185

Please sign in to comment.