Browse Source

Improve test coverage

Cover scenarios primarily for adjusted behavior on Button, as well as ensure components are exported correctly.
tags/0.3.0
TheoryOfNekomata 4 years ago
parent
commit
f83cd94cda
24 changed files with 534 additions and 520 deletions
  1. +0
    -1
      .npmignore
  2. +1
    -1
      .prettierrc
  3. +51
    -13
      lib/components/Button/Button.test.tsx
  4. +147
    -71
      lib/components/Button/Button.tsx
  5. +3
    -0
      lib/components/Checkbox/Checkbox.test.tsx
  6. +13
    -13
      lib/components/Checkbox/Checkbox.tsx
  7. +3
    -0
      lib/components/Icon/Icon.test.tsx
  8. +4
    -17
      lib/components/Icon/Icon.tsx
  9. +3
    -0
      lib/components/RadioButton/RadioButton.test.tsx
  10. +33
    -28
      lib/components/RadioButton/RadioButton.tsx
  11. +3
    -27
      lib/components/Select/Select.test.tsx
  12. +35
    -51
      lib/components/Select/Select.tsx
  13. +3
    -0
      lib/components/Slider/Slider.test.tsx
  14. +37
    -37
      lib/components/Slider/Slider.tsx
  15. +7
    -38
      lib/components/TextInput/TextInput.test.tsx
  16. +118
    -135
      lib/components/TextInput/TextInput.tsx
  17. +26
    -0
      lib/index.test.ts
  18. +6
    -12
      lib/services/isEmpty.test.ts
  19. +1
    -4
      lib/services/isEmpty.ts
  20. +14
    -27
      lib/services/splitValueAndUnit.test.ts
  21. +3
    -3
      lib/services/splitValueAndUnit.ts
  22. +18
    -37
      lib/services/stringify.test.ts
  23. +4
    -4
      lib/services/stringify.ts
  24. +1
    -1
      package.json

+ 0
- 1
.npmignore View File

@@ -8,7 +8,6 @@ utilities/
.editorconfig .editorconfig
.prettierrc .prettierrc
doczrc.js doczrc.js
global.d.ts
jest.config.js jest.config.js
jest.setup.ts jest.setup.ts
plopfile.js plopfile.js


+ 1
- 1
.prettierrc View File

@@ -4,6 +4,6 @@
"printWidth": 120, "printWidth": 120,
"semi": false, "semi": false,
"trailingComma": "all", "trailingComma": "all",
"quoteProps": "consistent",
"quoteProps": "as-needed",
"arrowParens": "always" "arrowParens": "always"
} }

+ 51
- 13
lib/components/Button/Button.test.tsx View File

@@ -1,16 +1,17 @@
/// <reference types="jest-enzyme" />
/// <reference path="../../../utilities/jest/extensions.ts" />

import * as fc from 'fast-check' import * as fc from 'fast-check'
import * as Enzyme from 'enzyme' import * as Enzyme from 'enzyme'
import * as Axe from 'jest-axe' import * as Axe from 'jest-axe'
import * as React from 'react' import * as React from 'react'
import Button, { Variant } from './Button'
import Button, { Variant, ButtonElement } from './Button'
import stringify from '../../services/stringify' import stringify from '../../services/stringify'


const CUSTOM_VARIANTS: string[] = ['primary'] const CUSTOM_VARIANTS: string[] = ['primary']


const AVAILABLE_VARIANTS: string[] = ['outline', 'primary'] const AVAILABLE_VARIANTS: string[] = ['outline', 'primary']


const BLOCK_DISPLAYS = ['block', 'grid', 'flex', 'table']

it('should exist', () => { it('should exist', () => {
expect(Button).toBeDefined() expect(Button).toBeDefined()
}) })
@@ -23,21 +24,46 @@ it('should render without crashing given minimum required props', () => {
expect(() => <Button />).not.toThrow() expect(() => <Button />).not.toThrow()
}) })


it('should render a border', () => {
const wrapper = Enzyme.shallow(<Button />)
expect(wrapper.find('button').find('span')).toHaveLength(1)
it('should render a button element for button kind', () => {
const wrapper = Enzyme.shallow(<Button element="button" />)
expect(wrapper.name()).toBe('button')
})

it('should render an a element for anchor kind', () => {
const wrapper = Enzyme.shallow(<Button element="a" />)
expect(wrapper.name()).toBe('a')
}) })


it('should render fullwidth when declared as block', () => {
const wrapper = Enzyme.shallow(<Button block />)
describe('on unknown kinds', () => {
let originalConsoleError: typeof console.error


expect(wrapper.find('button')).toHaveStyle('width', '100%')
beforeEach(() => {
originalConsoleError = console.error
console.error = () => {}
})

afterEach(() => {
console.error = originalConsoleError
})

it('should render null', () => {
fc.assert(
fc.property(fc.string().filter(s => !['button', 'a'].includes(s)), (element) => {
const wrapper = Enzyme.shallow(<Button element={element as ButtonElement} />)

expect(wrapper.isEmptyRender()).toBe(true)
}),
{
numRuns: 300,
},
)
})
}) })


it('should render as block element when declared as block', () => {
const wrapper = Enzyme.shallow(<Button block />)


expect(BLOCK_DISPLAYS).toContain(wrapper.find('button').prop('style')!.display)
it('should render a border', () => {
const wrapper = Enzyme.shallow(<Button />)
expect(wrapper.find('button').find('span')).toHaveLength(1)
}) })


it('should render a label', () => { it('should render a label', () => {
@@ -53,6 +79,18 @@ it('should render a label', () => {
) )
}) })


describe('on disabled', () => {
it('should render a disabled button element for button kind', () => {
const wrapper = Enzyme.shallow(<Button disabled element="button" />)
expect(wrapper.find('button')).toHaveProp('disabled', true)
})

it('should render a span element for anchor kind', () => {
const wrapper = Enzyme.shallow(<Button disabled element="a" />)
expect(wrapper.name()).toBe('span')
})
})

describe.each(AVAILABLE_VARIANTS)('with %p variant', (rawVariant) => { describe.each(AVAILABLE_VARIANTS)('with %p variant', (rawVariant) => {
const variant = rawVariant as Variant const variant = rawVariant as Variant


@@ -77,7 +115,7 @@ describe.each(AVAILABLE_VARIANTS)('with %p variant', (rawVariant) => {
}) })


describe('with unknown variants', () => { describe('with unknown variants', () => {
let originalConsoleError: any
let originalConsoleError: typeof console.error


beforeEach(() => { beforeEach(() => {
// silence console.error() from prop type validation since // silence console.error() from prop type validation since


+ 147
- 71
lib/components/Button/Button.tsx View File

@@ -1,64 +1,86 @@
import * as React from 'react' import * as React from 'react'
import * as PropTypes from 'prop-types' import * as PropTypes from 'prop-types'
import styled from 'styled-components'
import styled, { CSSObject } from 'styled-components'
import { Size, SizeMap } from '../../services/utilities' import { Size, SizeMap } from '../../services/utilities'
import stringify from '../../services/stringify' import stringify from '../../services/stringify'


export type Variant = 'outline' | 'primary' export type Variant = 'outline' | 'primary'


export type ButtonElement = 'a' | 'button'

type ButtonType = 'submit' | 'reset' | 'button'

const MIN_HEIGHTS: SizeMap<string | number> = { const MIN_HEIGHTS: SizeMap<string | number> = {
small: '2.5rem', small: '2.5rem',
medium: '3rem', medium: '3rem',
large: '4rem', large: '4rem',
} }


const Base = styled('button')({
'appearance': 'none',
'padding': '0 1rem',
'font': 'inherit',
'fontFamily': 'var(--font-family-base)',
'textTransform': 'uppercase',
'fontWeight': 'bolder',
'borderRadius': '0.25rem',
'placeContent': 'center',
'position': 'relative',
'cursor': 'pointer',
'border': 0,
'userSelect': 'none',
'textDecoration': 'none',
'transitionProperty': 'background-color, color',
'whiteSpace': 'nowrap',
'lineHeight': 1,
const disabledButtonStyles: CSSObject = {
opacity: 0.5,
cursor: 'not-allowed',
}

const buttonStyles: CSSObject = {
display: 'grid',
appearance: 'none',
padding: '0 1rem',
font: 'inherit',
fontFamily: 'var(--font-family-base)',
textTransform: 'uppercase',
fontWeight: 'bolder',
borderRadius: '0.25rem',
placeContent: 'center',
position: 'relative',
cursor: 'pointer',
border: 0,
userSelect: 'none',
textDecoration: 'none',
transitionProperty: 'background-color, color',
whiteSpace: 'nowrap',
lineHeight: 1,
':focus': { ':focus': {
'--color-accent': 'var(--color-active, Highlight)', '--color-accent': 'var(--color-active, Highlight)',
'outline': 0,
},
':disabled': {
opacity: 0.5,
cursor: 'not-allowed',
outline: 0,
}, },
':disabled': disabledButtonStyles,
'::-moz-focus-inner': { '::-moz-focus-inner': {
outline: 0, outline: 0,
border: 0, border: 0,
}, },
})
}

const disabledLinkButtonStyles: CSSObject = {
...buttonStyles,
...disabledButtonStyles,
}

const Base = styled('button')(buttonStyles)


Base.displayName = 'button' Base.displayName = 'button'


const LinkBase = styled('a')(buttonStyles)

LinkBase.displayName = 'a'

const DisabledLinkBase = styled('span')(disabledLinkButtonStyles)

DisabledLinkBase.displayName = 'span'

const Border = styled('span')({ const Border = styled('span')({
'borderColor': 'var(--color-accent, blue)',
'boxSizing': 'border-box',
'display': 'inline-block',
'borderWidth': '0.125rem',
'borderStyle': 'solid',
'position': 'absolute',
'top': 0,
'left': 0,
'width': '100%',
'height': '100%',
'borderRadius': 'inherit',
'pointerEvents': 'none',
'transitionProperty': 'border-color',
borderColor: 'var(--color-accent, blue)',
boxSizing: 'border-box',
display: 'inline-block',
borderWidth: '0.125rem',
borderStyle: 'solid',
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
borderRadius: 'inherit',
pointerEvents: 'none',
transitionProperty: 'border-color',
'::before': { '::before': {
position: 'absolute', position: 'absolute',
top: 0, top: 0,
@@ -77,6 +99,19 @@ const Border = styled('span')({


Border.displayName = 'span' Border.displayName = 'span'


const defaultVariantStyleSet: React.CSSProperties = {
backgroundColor: 'transparent',
color: 'var(--color-accent, blue)',
}

const variantStyleSets: Record<Variant, React.CSSProperties> = {
outline: defaultVariantStyleSet,
primary: {
backgroundColor: 'var(--color-accent, blue)',
color: 'var(--color-bg, white)',
},
}

const propTypes = { const propTypes = {
/** /**
* Size of the component. * Size of the component.
@@ -85,11 +120,7 @@ const propTypes = {
/** /**
* Variant of the component. * Variant of the component.
*/ */
variant: PropTypes.oneOf<Exclude<Variant, 'unknown'>>(['outline', 'primary']),
/**
* Should the component take up the remaining space parallel to the content flow?
*/
block: PropTypes.bool,
variant: PropTypes.oneOf<Variant>(['outline', 'primary']),
/** /**
* Text to identify the action associated upon activation of the component. * Text to identify the action associated upon activation of the component.
*/ */
@@ -98,43 +129,88 @@ const propTypes = {
* Can the component be activated? * Can the component be activated?
*/ */
disabled: PropTypes.bool, disabled: PropTypes.bool,
/**
* The corresponding HTML element of the component.
*/
element: PropTypes.oneOf<ButtonElement>(['a', 'button']),
/**
* The URL of the page to navigate to, if element is set to "a".
*/
href: PropTypes.string,
/**
* The target on where to display the page navigated to, if element is set to "a".
*/
target: PropTypes.string,
/**
* The relationship of the current page to the referred page in "href", if element is set to "a".
*/
rel: PropTypes.string,
/**
* The type of the button, if element is set to "button".
*/
type: PropTypes.oneOf<ButtonType>(['submit', 'reset', 'button']),
} }


type Props = PropTypes.InferProps<typeof propTypes> type Props = PropTypes.InferProps<typeof propTypes>


const defaultVariantStyleSet: React.CSSProperties = {
backgroundColor: 'transparent',
color: 'var(--color-accent, blue)',
}

const variantStyleSets: Record<Variant, React.CSSProperties> = {
outline: defaultVariantStyleSet,
primary: {
backgroundColor: 'var(--color-accent, blue)',
color: 'var(--color-bg, white)',
},
}

const Button = React.forwardRef<HTMLButtonElement, Props>(
({ size = 'medium', variant = 'outline', block = false, disabled = false, children, ...etcProps }, ref) => {
const Button = React.forwardRef<HTMLAnchorElement | HTMLButtonElement | HTMLSpanElement, Props>(
(
{
size = 'medium',
variant = 'outline',
disabled = false,
children,
element = 'button',
href,
target,
rel,
type = 'button',
},
ref,
) => {
const { [variant as Variant]: theVariantStyleSet = defaultVariantStyleSet } = variantStyleSets const { [variant as Variant]: theVariantStyleSet = defaultVariantStyleSet } = variantStyleSets

return (
<Base
{...etcProps}
ref={ref}
disabled={disabled!}
style={{
...theVariantStyleSet,
minHeight: MIN_HEIGHTS[size as Size],
width: block ? '100%' : undefined,
display: block ? 'grid' : 'inline-grid',
}}
>
const commonButtonStyles: React.CSSProperties = {
...theVariantStyleSet,
minHeight: MIN_HEIGHTS[size!],
}
const buttonContent = (
<React.Fragment>
<Border /> <Border />
{stringify(children)} {stringify(children)}
</Base>
</React.Fragment>
) )

switch (element) {
case 'button':
return (
<Base type={type!} ref={ref as React.Ref<HTMLButtonElement>} disabled={disabled!} style={commonButtonStyles}>
{buttonContent}
</Base>
)
case 'a':
if (disabled) {
return (
<DisabledLinkBase ref={ref as React.Ref<HTMLSpanElement>} style={commonButtonStyles}>
{buttonContent}
</DisabledLinkBase>
)
}
return (
<LinkBase
href={href!}
target={target!}
rel={rel!}
ref={ref as React.Ref<HTMLAnchorElement>}
style={commonButtonStyles}
>
{buttonContent}
</LinkBase>
)
default:
break
}

return null
}, },
) )




+ 3
- 0
lib/components/Checkbox/Checkbox.test.tsx View File

@@ -1,3 +1,6 @@
/// <reference types="jest-enzyme" />
/// <reference path="../../../utilities/jest/extensions.ts" />

import * as fc from 'fast-check' import * as fc from 'fast-check'
import * as Enzyme from 'enzyme' import * as Enzyme from 'enzyme'
import * as Axe from 'jest-axe' import * as Axe from 'jest-axe'


+ 13
- 13
lib/components/Checkbox/Checkbox.tsx View File

@@ -9,7 +9,7 @@ const Base = styled('div')({
}) })


const CaptureArea = styled('label')({ const CaptureArea = styled('label')({
'marginTop': '0.25rem',
marginTop: '0.25rem',
'::after': { '::after': {
content: '""', content: '""',
}, },
@@ -52,18 +52,18 @@ const IndicatorWrapper = styled('span')({
}) })


const Border = styled('span')({ const Border = styled('span')({
'borderColor': 'var(--color-accent, blue)',
'boxSizing': 'border-box',
'display': 'inline-block',
'borderWidth': '0.125rem',
'borderStyle': 'solid',
'position': 'absolute',
'top': 0,
'left': 0,
'width': '100%',
'height': '100%',
'borderRadius': 'inherit',
'transitionProperty': 'border-color',
borderColor: 'var(--color-accent, blue)',
boxSizing: 'border-box',
display: 'inline-block',
borderWidth: '0.125rem',
borderStyle: 'solid',
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
borderRadius: 'inherit',
transitionProperty: 'border-color',
'::before': { '::before': {
position: 'absolute', position: 'absolute',
top: 0, top: 0,


+ 3
- 0
lib/components/Icon/Icon.test.tsx View File

@@ -1,3 +1,6 @@
/// <reference types="jest-enzyme" />
/// <reference path="../../../utilities/jest/extensions.ts" />

import * as fc from 'fast-check' import * as fc from 'fast-check'
import * as Enzyme from 'enzyme' import * as Enzyme from 'enzyme'
import * as React from 'react' import * as React from 'react'


+ 4
- 17
lib/components/Icon/Icon.tsx View File

@@ -6,10 +6,10 @@ import { pascalCase, pascalCaseTransformMerge } from 'pascal-case'
import splitValueAndUnit from '../../services/splitValueAndUnit' import splitValueAndUnit from '../../services/splitValueAndUnit'


const Label = styled('span')({ const Label = styled('span')({
'position': 'absolute',
'left': -999999,
'width': 1,
'height': 1,
position: 'absolute',
left: -999999,
width: 1,
height: 1,
':empty': { ':empty': {
display: 'none', display: 'none',
}, },
@@ -51,19 +51,6 @@ const propTypes = {


type Props = PropTypes.InferProps<typeof propTypes> type Props = PropTypes.InferProps<typeof propTypes>


/**
* Component for displaying graphical icons.
*
* @see {@link //feathericons.com|Feather Icons} for a complete list of icons.
* @param {string} name - Name of the icon to display.
* @param {string} weight - Width of the icon's strokes.
* @param {string | number} [size] - Size of the icon. This controls both the width and the height.
* @param {CSSProperties} [style] - CSS style of the icon. For icon dimensions, use `size` instead.
* @param {string} [label] - Describe of what the component represents.
* @param {string} [className] - Class name used for styling.
* @param {object} etcProps - The rest of the props.
* @returns {React.ReactElement | null} - The component elements.
*/
const Icon: React.FC<Props> = ({ const Icon: React.FC<Props> = ({
name, name,
weight = '0.125rem', weight = '0.125rem',


+ 3
- 0
lib/components/RadioButton/RadioButton.test.tsx View File

@@ -1,3 +1,6 @@
/// <reference types="jest-enzyme" />
/// <reference path="../../../utilities/jest/extensions.ts" />

import * as fc from 'fast-check' import * as fc from 'fast-check'
import * as Enzyme from 'enzyme' import * as Enzyme from 'enzyme'
import * as Axe from 'jest-axe' import * as Axe from 'jest-axe'


+ 33
- 28
lib/components/RadioButton/RadioButton.tsx View File

@@ -8,7 +8,7 @@ const Base = styled('div')({
}) })


const CaptureArea = styled('label')({ const CaptureArea = styled('label')({
'marginTop': '0.25rem',
marginTop: '0.25rem',
'::after': { '::after': {
content: '""', content: '""',
clear: 'both', clear: 'both',
@@ -52,18 +52,18 @@ const IndicatorWrapper = styled('span')({
}) })


const Border = styled('span')({ const Border = styled('span')({
'borderColor': 'var(--color-accent, blue)',
'boxSizing': 'border-box',
'display': 'inline-block',
'borderWidth': '0.125rem',
'borderStyle': 'solid',
'position': 'absolute',
'top': 0,
'left': 0,
'width': '100%',
'height': '100%',
'borderRadius': 'inherit',
'transitionProperty': 'border-color',
borderColor: 'var(--color-accent, blue)',
boxSizing: 'border-box',
display: 'inline-block',
borderWidth: '0.125rem',
borderStyle: 'solid',
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
borderRadius: 'inherit',
transitionProperty: 'border-color',
'::before': { '::before': {
position: 'absolute', position: 'absolute',
top: 0, top: 0,
@@ -131,21 +131,26 @@ type Props = PropTypes.InferProps<typeof propTypes>
* @see {@link Select} for a similar component suitable for selecting more values. * @see {@link Select} for a similar component suitable for selecting more values.
* @type {React.ComponentType<{readonly label?: string, readonly name?: string} & React.ClassAttributes<unknown>>} * @type {React.ComponentType<{readonly label?: string, readonly name?: string} & React.ClassAttributes<unknown>>}
*/ */
const RadioButton = React.forwardRef<HTMLInputElement, Props>(({ label = '', name, ...etcProps }, ref) => (
<Base>
<CaptureArea>
<Input {...etcProps} ref={ref} name={name} type="radio" />
<IndicatorWrapper>
<Border />
<Indicator />
</IndicatorWrapper>
{typeof label! !== 'undefined' && label !== null && ' '}
<Label>
<LabelContent>{stringify(label)}</LabelContent>
</Label>
</CaptureArea>
</Base>
))
const RadioButton = React.forwardRef<HTMLInputElement, Props>(
(
{ label = '', name, ...etcProps },
ref
) => (
<Base>
<CaptureArea>
<Input {...etcProps} ref={ref} name={name} type="radio" />
<IndicatorWrapper>
<Border />
<Indicator />
</IndicatorWrapper>
{typeof label! !== 'undefined' && label !== null && ' '}
<Label>
<LabelContent>{stringify(label)}</LabelContent>
</Label>
</CaptureArea>
</Base>
)
)


RadioButton.propTypes = propTypes RadioButton.propTypes = propTypes




+ 3
- 27
lib/components/Select/Select.test.tsx View File

@@ -1,3 +1,6 @@
/// <reference types="jest-enzyme" />
/// <reference path="../../../utilities/jest/extensions.ts" />

import * as fc from 'fast-check' import * as fc from 'fast-check'
import * as Enzyme from 'enzyme' import * as Enzyme from 'enzyme'
import * as Axe from 'jest-axe' import * as Axe from 'jest-axe'
@@ -68,33 +71,6 @@ it('should render a hint for describing valid input values', () => {
) )
}) })


describe('on being declared a block component', () => {
const BLOCK_DISPLAYS = ['block', 'grid', 'flex', 'table']

it('should render the base element fullwidth', () => {
const wrapper = Enzyme.shallow(<Select block />)

expect(BLOCK_DISPLAYS).toContain(wrapper.find('div').prop('style')!.display)
})

it('should render the input fullwidth', () => {
const wrapper = Enzyme.shallow(<Select block />)

expect(wrapper.find('label').find('select')).toHaveStyle('width', '100%')
})

it('should render the input as block element', () => {
const wrapper = Enzyme.shallow(<Select block />)

expect(BLOCK_DISPLAYS).toContain(
wrapper
.find('label')
.find('select')
.prop('style')!.display,
)
})
})

it('should render as half-opaque when disabled', () => { it('should render as half-opaque when disabled', () => {
const wrapper = Enzyme.shallow(<Select disabled />) const wrapper = Enzyme.shallow(<Select disabled />)




+ 35
- 51
lib/components/Select/Select.tsx View File

@@ -36,10 +36,10 @@ const SECONDARY_TEXT_SIZES: SizeMap<string | number> = {
} }


const ComponentBase = styled('div')({ const ComponentBase = styled('div')({
'position': 'relative',
'borderRadius': '0.25rem',
'fontFamily': 'var(--font-family-base)',
'maxWidth': '100%',
position: 'relative',
borderRadius: '0.25rem',
fontFamily: 'var(--font-family-base)',
maxWidth: '100%',
':focus-within': { ':focus-within': {
'--color-accent': 'var(--color-active, Highlight)', '--color-accent': 'var(--color-active, Highlight)',
}, },
@@ -76,21 +76,22 @@ const LabelWrapper = styled('span')({
LabelWrapper.displayName = 'span' LabelWrapper.displayName = 'span'


const Input = styled('select')({ const Input = styled('select')({
'backgroundColor': 'var(--color-bg, white)',
'color': 'var(--color-fg, black)',
'appearance': 'none',
'boxSizing': 'border-box',
'position': 'relative',
'border': 0,
'paddingLeft': '1rem',
'margin': 0,
'font': 'inherit',
'minHeight': '4rem',
'minWidth': '16rem',
'maxWidth': '100%',
'zIndex': 1,
'cursor': 'pointer',
'transitionProperty': 'background-color, color',
display: 'block',
backgroundColor: 'var(--color-bg, white)',
color: 'var(--color-fg, black)',
appearance: 'none',
boxSizing: 'border-box',
position: 'relative',
border: 0,
paddingLeft: '1rem',
margin: 0,
font: 'inherit',
minHeight: '4rem',
minWidth: '8rem',
maxWidth: '100%',
zIndex: 1,
cursor: 'pointer',
transitionProperty: 'background-color, color',
':focus': { ':focus': {
outline: 0, outline: 0,
}, },
@@ -106,20 +107,20 @@ const Input = styled('select')({
Input.displayName = 'select' Input.displayName = 'select'


const Border = styled('span')({ const Border = styled('span')({
'borderColor': 'var(--color-accent, blue)',
'boxSizing': 'border-box',
'display': 'inline-block',
'borderWidth': '0.125rem',
'borderStyle': 'solid',
'position': 'absolute',
'top': 0,
'left': 0,
'width': '100%',
'height': '100%',
'borderRadius': 'inherit',
'zIndex': 2,
'pointerEvents': 'none',
'transitionProperty': 'border-color',
borderColor: 'var(--color-accent, blue)',
boxSizing: 'border-box',
display: 'inline-block',
borderWidth: '0.125rem',
borderStyle: 'solid',
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
borderRadius: 'inherit',
zIndex: 2,
pointerEvents: 'none',
transitionProperty: 'border-color',
'::before': { '::before': {
position: 'absolute', position: 'absolute',
top: 0, top: 0,
@@ -188,10 +189,6 @@ const propTypes = {
* Size of the component. * Size of the component.
*/ */
size: PropTypes.oneOf<Size>(['small', 'medium', 'large']), size: PropTypes.oneOf<Size>(['small', 'medium', 'large']),
/**
* Should the component take up the remaining space parallel to the content flow?
*/
block: PropTypes.bool,
/** /**
* Can multiple values be selected? * Can multiple values be selected?
*/ */
@@ -208,22 +205,13 @@ const propTypes = {


type Props = PropTypes.InferProps<typeof propTypes> type Props = PropTypes.InferProps<typeof propTypes>


/**
* Component for selecting values from a larger number of options.
* @see {@link Checkbox} for a similar component on selecting values among very few choices.
* @see {@link RadioButton} for a similar component on selecting a single value among very few choices.
* @type {React.ComponentType<{readonly label?: string, readonly hint?: string, readonly className?: string, readonly
* size?: 'small' | 'medium' | 'large', readonly multiple?: boolean, readonly block?: boolean} &
* React.ClassAttributes<unknown>>}
*/
const Select = React.forwardRef<HTMLSelectElement, Props>( const Select = React.forwardRef<HTMLSelectElement, Props>(
( (
{ {
label = '', label = '',
className = '', className = '',
hint = '', hint = '',
size: sizeProp = 'medium',
block = false,
size = 'medium',
multiple = false, multiple = false,
disabled = false, disabled = false,
style = {}, style = {},
@@ -231,11 +219,9 @@ const Select = React.forwardRef<HTMLSelectElement, Props>(
}, },
ref, ref,
) => { ) => {
const size = sizeProp as Size
return ( return (
<ComponentBase <ComponentBase
style={{ style={{
display: block ? 'block' : 'inline-block',
opacity: disabled ? 0.5 : undefined, opacity: disabled ? 0.5 : undefined,
}} }}
> >
@@ -259,10 +245,8 @@ const Select = React.forwardRef<HTMLSelectElement, Props>(
multiple={multiple!} multiple={multiple!}
style={{ style={{
...style, ...style,
display: block ? 'block' : 'inline-block',
verticalAlign: 'top', verticalAlign: 'top',
fontSize: INPUT_FONT_SIZES[size!], fontSize: INPUT_FONT_SIZES[size!],
width: block || multiple ? '100%' : undefined,
height: multiple ? undefined : MIN_HEIGHTS[size!], height: multiple ? undefined : MIN_HEIGHTS[size!],
minHeight: MIN_HEIGHTS[size!], minHeight: MIN_HEIGHTS[size!],
resize: multiple ? 'vertical' : undefined, resize: multiple ? 'vertical' : undefined,


+ 3
- 0
lib/components/Slider/Slider.test.tsx View File

@@ -1,3 +1,6 @@
/// <reference types="jest-enzyme" />
/// <reference path="../../../utilities/jest/extensions.ts" />

import * as fc from 'fast-check' import * as fc from 'fast-check'
import * as Enzyme from 'enzyme' import * as Enzyme from 'enzyme'
import * as React from 'react' import * as React from 'react'


+ 37
- 37
lib/components/Slider/Slider.tsx View File

@@ -5,9 +5,9 @@ import styled from 'styled-components'
import stringify from '../../services/stringify' import stringify from '../../services/stringify'


const Wrapper = styled('div')({ const Wrapper = styled('div')({
'position': 'relative',
'display': 'inline-block',
'verticalAlign': 'top',
position: 'relative',
display: 'inline-block',
verticalAlign: 'top',
':focus-within': { ':focus-within': {
'--color-accent': 'var(--color-active, Highlight)', '--color-accent': 'var(--color-active, Highlight)',
}, },
@@ -16,10 +16,10 @@ const Wrapper = styled('div')({
Wrapper.displayName = 'div' Wrapper.displayName = 'div'


const Base = styled(ReachSlider.SliderInput)({ const Base = styled(ReachSlider.SliderInput)({
'boxSizing': 'border-box',
'position': 'absolute',
'bottom': 0,
'right': 0,
boxSizing: 'border-box',
position: 'absolute',
bottom: 0,
right: 0,
':active': { ':active': {
cursor: 'grabbing', cursor: 'grabbing',
}, },
@@ -28,10 +28,10 @@ const Base = styled(ReachSlider.SliderInput)({
Base.displayName = 'input' Base.displayName = 'input'


const Track = styled(ReachSlider.SliderTrack)({ const Track = styled(ReachSlider.SliderTrack)({
'borderRadius': '0.125rem',
'position': 'relative',
'width': '100%',
'height': '100%',
borderRadius: '0.125rem',
position: 'relative',
width: '100%',
height: '100%',
'::before': { '::before': {
display: 'block', display: 'block',
position: 'absolute', position: 'absolute',
@@ -45,15 +45,15 @@ const Track = styled(ReachSlider.SliderTrack)({
}) })


const Handle = styled(ReachSlider.SliderHandle)({ const Handle = styled(ReachSlider.SliderHandle)({
'cursor': 'grab',
'width': '1.25rem',
'height': '1.25rem',
'backgroundColor': 'var(--color-accent, blue)',
'borderRadius': '50%',
'zIndex': 1,
'transformOrigin': 'center',
'outline': 0,
'position': 'relative',
cursor: 'grab',
width: '1.25rem',
height: '1.25rem',
backgroundColor: 'var(--color-accent, blue)',
borderRadius: '50%',
zIndex: 1,
transformOrigin: 'center',
outline: 0,
position: 'relative',
'::before': { '::before': {
position: 'absolute', position: 'absolute',
top: 0, top: 0,
@@ -113,12 +113,12 @@ const LabelWrapper = styled('span')({
LabelWrapper.displayName = 'span' LabelWrapper.displayName = 'span'


const FallbackTrack = styled('span')({ const FallbackTrack = styled('span')({
'padding': '1.875rem 0.75rem 0.875rem',
'boxSizing': 'border-box',
'position': 'absolute',
'top': 0,
'left': 0,
'pointerEvents': 'none',
padding: '1.875rem 0.75rem 0.875rem',
boxSizing: 'border-box',
position: 'absolute',
top: 0,
left: 0,
pointerEvents: 'none',
'::before': { '::before': {
display: 'block', display: 'block',
content: "''", content: "''",
@@ -131,17 +131,17 @@ const FallbackTrack = styled('span')({
}) })


const FallbackSlider = styled('input')({ const FallbackSlider = styled('input')({
'boxSizing': 'border-box',
'backgroundColor': 'transparent',
'verticalAlign': 'top',
'margin': 0,
'width': '100%',
'height': '2rem',
'marginTop': '1rem',
'appearance': 'none',
'outline': 0,
'position': 'absolute',
'left': 0,
boxSizing: 'border-box',
backgroundColor: 'transparent',
verticalAlign: 'top',
margin: 0,
width: '100%',
height: '2rem',
marginTop: '1rem',
appearance: 'none',
outline: 0,
position: 'absolute',
left: 0,
'::-moz-focus-inner': { '::-moz-focus-inner': {
outline: 0, outline: 0,
border: 0, border: 0,


+ 7
- 38
lib/components/TextInput/TextInput.test.tsx View File

@@ -1,3 +1,6 @@
/// <reference types="jest-enzyme" />
/// <reference path="../../../utilities/jest/extensions.ts" />

import * as fc from 'fast-check' import * as fc from 'fast-check'
import * as Enzyme from 'enzyme' import * as Enzyme from 'enzyme'
import * as Axe from 'jest-axe' import * as Axe from 'jest-axe'
@@ -64,13 +67,12 @@ it('should render as half-opaque when disabled', () => {
}) })


describe.each` describe.each`
multiline | inputWidth | tag
${false} | ${'100%'} | ${'input'}
${true} | ${undefined} | ${'textarea'}
`('on multiline (multiline=$multiline)', ({ tag: rawTag, multiline: rawMultiline, inputWidth: rawInputWidth }) => {
multiline | tag
${false} | ${'input'}
${true} | ${'textarea'}
`('on multiline (multiline=$multiline)', ({ tag: rawTag, multiline: rawMultiline, }) => {
const tag = rawTag as string const tag = rawTag as string
const multiline = rawMultiline as boolean const multiline = rawMultiline as boolean
const inputWidth = rawInputWidth as string


it('should render an element to input text on', () => { it('should render an element to input text on', () => {
const wrapper = Enzyme.shallow(<TextInput multiline={multiline} />) const wrapper = Enzyme.shallow(<TextInput multiline={multiline} />)
@@ -99,39 +101,6 @@ describe.each`
.prop('style')!.paddingRight, .prop('style')!.paddingRight,
).not.toBe('1rem') ).not.toBe('1rem')
}) })

describe('on being declared a block component', () => {
const BLOCK_DISPLAYS = ['block', 'grid', 'flex', 'table']

it('should render the base element fullwidth', () => {
const wrapper = Enzyme.shallow(<TextInput multiline={multiline} block />)

const { display } = wrapper.find('div').prop('style')!

expect(BLOCK_DISPLAYS).toContain(display)
})

it('should render the input fullwidth', () => {
const wrapper = Enzyme.shallow(<TextInput multiline={multiline} block />)

if (tag === 'textarea') {
expect(wrapper.find('label').find(tag)).not.toHaveStyle('width')
} else {
expect(wrapper.find('label').find(tag)).toHaveStyle('width', inputWidth)
}
})

it('should render the input as block element', () => {
const wrapper = Enzyme.shallow(<TextInput multiline={multiline} block />)

const { display } = wrapper
.find('label')
.find(tag)
.prop('style')!

expect(BLOCK_DISPLAYS).toContain(display)
})
})
}) })


describe('on aiding user input', () => { describe('on aiding user input', () => {


+ 118
- 135
lib/components/TextInput/TextInput.tsx View File

@@ -3,7 +3,6 @@ import * as PropTypes from 'prop-types'
import styled from 'styled-components' import styled from 'styled-components'
import stringify from '../../services/stringify' import stringify from '../../services/stringify'
import { Size, SizeMap } from '../../services/utilities' import { Size, SizeMap } from '../../services/utilities'
import { ChangeEvent, InputHTMLAttributes, TextareaHTMLAttributes } from 'react'


const MIN_HEIGHTS: SizeMap<string | number> = { const MIN_HEIGHTS: SizeMap<string | number> = {
small: '2.5rem', small: '2.5rem',
@@ -36,10 +35,10 @@ const SECONDARY_TEXT_SIZES: SizeMap<string | number> = {
} }


const ComponentBase = styled('div')({ const ComponentBase = styled('div')({
'position': 'relative',
'borderRadius': '0.25rem',
'fontFamily': 'var(--font-family-base)',
'maxWidth': '100%',
position: 'relative',
borderRadius: '0.25rem',
fontFamily: 'var(--font-family-base)',
maxWidth: '100%',
':focus-within': { ':focus-within': {
'--color-accent': 'var(--color-active, Highlight)', '--color-accent': 'var(--color-active, Highlight)',
}, },
@@ -78,20 +77,20 @@ const LabelWrapper = styled('span')({
LabelWrapper.displayName = 'span' LabelWrapper.displayName = 'span'


const Border = styled('span')({ const Border = styled('span')({
'borderColor': 'var(--color-accent, blue)',
'boxSizing': 'border-box',
'display': 'inline-block',
'borderWidth': '0.125rem',
'borderStyle': 'solid',
'position': 'absolute',
'top': 0,
'left': 0,
'width': '100%',
'height': '100%',
'borderRadius': 'inherit',
'zIndex': 2,
'pointerEvents': 'none',
'transitionProperty': 'border-color',
borderColor: 'var(--color-accent, blue)',
boxSizing: 'border-box',
display: 'inline-block',
borderWidth: '0.125rem',
borderStyle: 'solid',
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
borderRadius: 'inherit',
zIndex: 2,
pointerEvents: 'none',
transitionProperty: 'border-color',
'::before': { '::before': {
position: 'absolute', position: 'absolute',
top: 0, top: 0,
@@ -109,23 +108,24 @@ const Border = styled('span')({
}) })


const Input = styled('input')({ const Input = styled('input')({
'backgroundColor': 'var(--color-bg, white)',
'color': 'var(--color-fg, black)',
'verticalAlign': 'top',
'paddingTop': 0,
'paddingBottom': 0,
'boxSizing': 'border-box',
'position': 'relative',
'border': 0,
'borderRadius': 'inherit',
'paddingLeft': '1rem',
'margin': 0,
'font': 'inherit',
'minHeight': '4rem',
'minWidth': '16rem',
'maxWidth': '100%',
'zIndex': 1,
'transitionProperty': 'background-color, color',
display: 'block',
backgroundColor: 'var(--color-bg, white)',
color: 'var(--color-fg, black)',
verticalAlign: 'top',
paddingTop: 0,
paddingBottom: 0,
boxSizing: 'border-box',
position: 'relative',
border: 0,
borderRadius: 'inherit',
paddingLeft: '1rem',
margin: 0,
font: 'inherit',
minHeight: '4rem',
minWidth: '8rem',
maxWidth: '100%',
zIndex: 1,
transitionProperty: 'background-color, color',
':focus': { ':focus': {
outline: 0, outline: 0,
color: 'var(--color-fg, black)', color: 'var(--color-fg, black)',
@@ -138,22 +138,23 @@ const Input = styled('input')({
Input.displayName = 'input' Input.displayName = 'input'


const TextArea = styled('textarea')({ const TextArea = styled('textarea')({
'backgroundColor': 'var(--color-bg, white)',
'color': 'var(--color-fg, black)',
'verticalAlign': 'top',
'width': '100%',
'boxSizing': 'border-box',
'position': 'relative',
'border': 0,
'borderRadius': 'inherit',
'paddingLeft': '1rem',
'margin': 0,
'font': 'inherit',
'minHeight': '4rem',
'minWidth': '16rem',
'maxWidth': '100%',
'zIndex': 1,
'transitionProperty': 'background-color, color',
display: 'block',
backgroundColor: 'var(--color-bg, white)',
color: 'var(--color-fg, black)',
verticalAlign: 'top',
width: '100%',
boxSizing: 'border-box',
position: 'relative',
border: 0,
borderRadius: 'inherit',
paddingLeft: '1rem',
margin: 0,
font: 'inherit',
minHeight: '4rem',
minWidth: '16rem',
maxWidth: '100%',
zIndex: 1,
transitionProperty: 'background-color, color',
':focus': { ':focus': {
outline: 0, outline: 0,
}, },
@@ -216,10 +217,6 @@ const propTypes = {
* Additional description, usually graphical, indicating the nature of the component's value. * Additional description, usually graphical, indicating the nature of the component's value.
*/ */
indicator: PropTypes.node, indicator: PropTypes.node,
/**
* Should the component take up the remaining space parallel to the content flow?
*/
block: PropTypes.bool,
/** /**
* Should the component accept multiple lines of input? * Should the component accept multiple lines of input?
*/ */
@@ -240,12 +237,6 @@ const propTypes = {


type Props = PropTypes.InferProps<typeof propTypes> type Props = PropTypes.InferProps<typeof propTypes>


/**
* Component for inputting textual values.
* @type {React.ComponentType<{readonly label?: string, readonly hint?: string, readonly multiline?: boolean, readonly
* className?: string, readonly indicator?: *, readonly size?: 'small' | 'medium' | 'large', readonly block?:
* boolean} & React.ClassAttributes<unknown>>}
*/
const TextInput = React.forwardRef<HTMLInputElement | HTMLTextAreaElement, Props>( const TextInput = React.forwardRef<HTMLInputElement | HTMLTextAreaElement, Props>(
( (
{ {
@@ -253,92 +244,84 @@ const TextInput = React.forwardRef<HTMLInputElement | HTMLTextAreaElement, Props
className = '', className = '',
hint = '', hint = '',
indicator = null, indicator = null,
size: sizeProp = 'medium',
block = false,
size = 'medium',
multiline = false, multiline = false,
disabled = false, disabled = false,
autoResize = false, autoResize = false,
placeholder = '', placeholder = '',
}, },
ref, ref,
) => {
const size: Size = sizeProp as Size
return (
<ComponentBase
style={{
display: block ? 'block' : 'inline-block',
opacity: disabled ? 0.5 : undefined,
}}
>
<Border />
<CaptureArea className={className!}>
<LabelWrapper
style={{
paddingTop: LABEL_VERTICAL_PADDING_SIZES[size!],
paddingBottom: LABEL_VERTICAL_PADDING_SIZES[size!],
paddingRight: indicator ? MIN_HEIGHTS[size!] : '0.5rem',
fontSize: SECONDARY_TEXT_SIZES[size!],
}}
>
{stringify(label)}
</LabelWrapper>
{stringify(label).length > 0 && ' '}
{multiline && (
<TextArea
placeholder={placeholder!}
ref={ref as React.Ref<HTMLTextAreaElement>}
disabled={disabled!}
style={{
display: block ? 'block' : 'inline-block',
fontSize: INPUT_FONT_SIZES[size!],
minHeight: MIN_HEIGHTS[size!],
paddingTop: VERTICAL_PADDING_SIZES[size!],
paddingBottom: VERTICAL_PADDING_SIZES[size!],
paddingRight: indicator ? MIN_HEIGHTS[size!] : '1rem',
}}
/>
)}
{!multiline && (
<Input
placeholder={placeholder!}
ref={ref as React.Ref<HTMLInputElement>}
disabled={disabled!}
style={{
display: block ? 'block' : 'inline-block',
fontSize: INPUT_FONT_SIZES[size!],
width: block ? '100%' : undefined,
minHeight: MIN_HEIGHTS[size!],
paddingRight: indicator ? MIN_HEIGHTS[size!] : '1rem',
}}
/>
)}
</CaptureArea>
{stringify(hint).length > 0 && ' '}
{stringify(hint).length > 0 && (
<HintWrapper
) => (
<ComponentBase
style={{
opacity: disabled ? 0.5 : undefined,
}}
>
<Border />
<CaptureArea className={className!}>
<LabelWrapper
style={{
paddingTop: LABEL_VERTICAL_PADDING_SIZES[size!],
paddingBottom: LABEL_VERTICAL_PADDING_SIZES[size!],
paddingRight: indicator ? MIN_HEIGHTS[size!] : '0.5rem',
fontSize: SECONDARY_TEXT_SIZES[size!],
}}
>
{stringify(label)}
</LabelWrapper>
{stringify(label).length > 0 && ' '}
{multiline && (
<TextArea
placeholder={placeholder!}
ref={ref as React.Ref<HTMLTextAreaElement>}
disabled={disabled!}
style={{ style={{
paddingTop: LABEL_VERTICAL_PADDING_SIZES[size!],
paddingBottom: LABEL_VERTICAL_PADDING_SIZES[size!],
fontSize: INPUT_FONT_SIZES[size!],
minHeight: MIN_HEIGHTS[size!],
paddingTop: VERTICAL_PADDING_SIZES[size!],
paddingBottom: VERTICAL_PADDING_SIZES[size!],
paddingRight: indicator ? MIN_HEIGHTS[size!] : '1rem', paddingRight: indicator ? MIN_HEIGHTS[size!] : '1rem',
fontSize: SECONDARY_TEXT_SIZES[size!],
}} }}
>
({stringify(hint)})
</HintWrapper>
/>
)} )}
{(indicator as PropTypes.ReactComponentLike) && (
<IndicatorWrapper
{!multiline && (
<Input
placeholder={placeholder!}
ref={ref as React.Ref<HTMLInputElement>}
disabled={disabled!}
style={{ style={{
width: MIN_HEIGHTS[size!],
height: MIN_HEIGHTS[size!],
fontSize: INPUT_FONT_SIZES[size!],
minHeight: MIN_HEIGHTS[size!],
paddingRight: indicator ? MIN_HEIGHTS[size!] : '1rem',
}} }}
>
{indicator}
</IndicatorWrapper>
/>
)} )}
</ComponentBase>
)
},
</CaptureArea>
{stringify(hint).length > 0 && ' '}
{stringify(hint).length > 0 && (
<HintWrapper
style={{
paddingTop: LABEL_VERTICAL_PADDING_SIZES[size!],
paddingBottom: LABEL_VERTICAL_PADDING_SIZES[size!],
paddingRight: indicator ? MIN_HEIGHTS[size!] : '1rem',
fontSize: SECONDARY_TEXT_SIZES[size!],
}}
>
({stringify(hint)})
</HintWrapper>
)}
{(indicator as PropTypes.ReactComponentLike) && (
<IndicatorWrapper
style={{
width: MIN_HEIGHTS[size!],
height: MIN_HEIGHTS[size!],
}}
>
{indicator}
</IndicatorWrapper>
)}
</ComponentBase>
),
) )


TextInput.propTypes = propTypes TextInput.propTypes = propTypes


+ 26
- 0
lib/index.test.ts View File

@@ -0,0 +1,26 @@
/// <reference types="jest-enzyme" />
/// <reference path="../utilities/jest/extensions.ts" />

import * as T from './index'

const Tesseract = T as Record<string, unknown>

describe.each`
name | componentId
${'button'} | ${'Button'}
${'checkbox'} | ${'Checkbox'}
${'icon'} | ${'Icon'}
${'radio button'} | ${'RadioButton'}
${'select'} | ${'Select'}
${'slider'} | ${'Slider'}
${'text input'} | ${'TextInput'}
`('on $name component', ({ componentId, }) => {
const theComponentId = componentId as string
it('should exist', () => {
expect(Tesseract[theComponentId]).toBeDefined()
})

it('should be a component', () => {
expect(Tesseract[theComponentId]).toBeComponent()
})
})

+ 6
- 12
lib/services/isEmpty.test.ts View File

@@ -16,12 +16,9 @@ describe('lib/services/isEmpty', () => {


it('should return a boolean value', () => { it('should return a boolean value', () => {
fc.assert( fc.assert(
fc.property(
fc.anything(),
v => {
expect(typeof isEmpty(v)).toBe('boolean')
}
)
fc.property(fc.anything(), (v) => {
expect(typeof isEmpty(v)).toBe('boolean')
}),
) )
}) })


@@ -36,12 +33,9 @@ describe('lib/services/isEmpty', () => {


it('should return `false` on an argument with value that is neither `undefined` nor `null`', () => { it('should return `false` on an argument with value that is neither `undefined` nor `null`', () => {
fc.assert( fc.assert(
fc.property(
fc.anything().filter(v => typeof v !== 'undefined' && v !== null),
v => {
expect(isEmpty(v)).toBe(false)
}
)
fc.property(fc.anything().filter((v) => typeof v !== 'undefined' && v !== null), (v) => {
expect(isEmpty(v)).toBe(false)
}),
) )
}) })
}) })


+ 1
- 4
lib/services/isEmpty.ts View File

@@ -2,9 +2,6 @@ interface IsEmpty {
(v: any): boolean (v: any): boolean
} }


const isEmpty: IsEmpty = v => (
typeof v === 'undefined'
|| v === null
)
const isEmpty: IsEmpty = (v) => typeof v === 'undefined' || v === null


export default isEmpty export default isEmpty

+ 14
- 27
lib/services/splitValueAndUnit.test.ts View File

@@ -1,5 +1,5 @@
import * as fc from 'fast-check' import * as fc from 'fast-check'
import splitValueAndUnit, { Unit, } from './splitValueAndUnit'
import splitValueAndUnit, { Unit } from './splitValueAndUnit'


it('should exist', () => { it('should exist', () => {
expect(splitValueAndUnit).toBeDefined() expect(splitValueAndUnit).toBeDefined()
@@ -15,46 +15,33 @@ it('should accept 1 argument', () => {


it('should throw a TypeError when invalid values are supplied', () => { it('should throw a TypeError when invalid values are supplied', () => {
fc.assert( fc.assert(
fc.property(
fc.anything().filter(s => !['string', 'number'].includes(typeof s)),
s => {
expect(() => splitValueAndUnit(s)).toThrowError(TypeError)
}
)
fc.property(fc.anything().filter((s) => !['string', 'number'].includes(typeof s)), (s) => {
expect(() => splitValueAndUnit(s)).toThrowError(TypeError)
}),
) )
}) })


it('should parse valid CSS numbers', () => { it('should parse valid CSS numbers', () => {
fc.assert( fc.assert(
fc.property( fc.property(
fc.tuple(
fc.float(),
fc.oneof<Unit>(
fc.constant('px'),
fc.constant('rem'),
fc.constant('%'),
)
),
([magnitude, unit,]) => {
fc.tuple(fc.float(), fc.oneof<Unit>(fc.constant('px'), fc.constant('rem'), fc.constant('%'))),
([magnitude, unit]) => {
expect(splitValueAndUnit(`${magnitude}${unit}`)).toEqual({ expect(splitValueAndUnit(`${magnitude}${unit}`)).toEqual({
magnitude, magnitude,
unit, unit,
}) })
}
)
},
),
) )
}) })


it('should parse numbers as CSS numbers with implicit pixel units', () => { it('should parse numbers as CSS numbers with implicit pixel units', () => {
fc.assert( fc.assert(
fc.property(
fc.float(),
magnitude => {
expect(splitValueAndUnit(magnitude)).toEqual({
magnitude,
unit: 'px',
})
}
)
fc.property(fc.float(), (magnitude) => {
expect(splitValueAndUnit(magnitude)).toEqual({
magnitude,
unit: 'px',
})
}),
) )
}) })

+ 3
- 3
lib/services/splitValueAndUnit.ts View File

@@ -1,15 +1,15 @@
export type Unit = 'px' | '%' | 'rem' export type Unit = 'px' | '%' | 'rem'


export interface ValueAndUnit { export interface ValueAndUnit {
magnitude: number,
unit: Unit,
magnitude: number
unit: Unit
} }


interface SplitValueAndUnit { interface SplitValueAndUnit {
(value: any): ValueAndUnit (value: any): ValueAndUnit
} }


const splitValueAndUnit: SplitValueAndUnit = value => {
const splitValueAndUnit: SplitValueAndUnit = (value) => {
if (!['string', 'number'].includes(typeof value)) { if (!['string', 'number'].includes(typeof value)) {
throw TypeError('Argument must be a valid CSS number') throw TypeError('Argument must be a valid CSS number')
} }


+ 18
- 37
lib/services/stringify.test.ts View File

@@ -16,12 +16,9 @@ it('should accept 1 argument', () => {


it('should return a string value', () => { it('should return a string value', () => {
fc.assert( fc.assert(
fc.property(
fc.anything(),
v => {
expect(stringify(v)).toBeString()
}
)
fc.property(fc.anything(), (v) => {
expect(stringify(v)).toBeString()
}),
) )
}) })


@@ -36,58 +33,42 @@ describe('on arguments', () => {


it('should stringify non-objects', () => { it('should stringify non-objects', () => {
fc.assert( fc.assert(
fc.property(
fcArb.nonObject(),
fc.string(),
v => {
expect(stringify(v)).toBe(String(v))
}
)
fc.property(fcArb.nonObject(), fc.string(), (v) => {
expect(stringify(v)).toBe(String(v))
}),
) )
}) })


it('should stringify objects', () => { it('should stringify objects', () => {
fc.assert( fc.assert(
fc.property(
fc.object(),
v => {
expect(stringify(v)).toBe(JSON.stringify(v))
}
)
fc.property(fc.object(), (v) => {
expect(stringify(v)).toBe(JSON.stringify(v))
}),
) )
}) })


describe('on arrays', () => { describe('on arrays', () => {
it('should stringify empty arrays', () => { it('should stringify empty arrays', () => {
fc.assert( fc.assert(
fc.property(
fc.array(fcArb.nonObject(), 0),
v => {
expect(stringify(v)).toBe('')
}
)
fc.property(fc.array(fcArb.nonObject(), 0), (v) => {
expect(stringify(v)).toBe('')
}),
) )
}) })


it('should stringify arrays with single values', () => { it('should stringify arrays with single values', () => {
fc.assert( fc.assert(
fc.property(
fc.array(fcArb.nonObject(), 1, 1),
v => {
expect(stringify(v)).toBe(String(v[0]))
}
)
fc.property(fc.array(fcArb.nonObject(), 1, 1), (v) => {
expect(stringify(v)).toBe(String(v[0]))
}),
) )
}) })


it('should stringify arrays with 2 or more values', () => { it('should stringify arrays with 2 or more values', () => {
fc.assert( fc.assert(
fc.property(
fc.array(fcArb.nonObject(), 2, 20),
v => {
expect(stringify(v)).toContain(',')
}
)
fc.property(fc.array(fcArb.nonObject(), 2, 20), (v) => {
expect(stringify(v)).toContain(',')
}),
) )
}) })
}) })


+ 4
- 4
lib/services/stringify.ts View File

@@ -1,18 +1,18 @@
import isEmpty from './isEmpty' import isEmpty from './isEmpty'


interface Stringify { interface Stringify {
(v: any): string,
(v: any): string
} }


const stringify: Stringify = v => {
const stringify: Stringify = (v) => {
if (isEmpty(v)) { if (isEmpty(v)) {
return '' return ''
} }


if (Array.isArray(v)) { if (Array.isArray(v)) {
return v return v
.filter(v => !isEmpty(v))
.map(v => stringify(v))
.filter((v) => !isEmpty(v))
.map((v) => stringify(v))
.join(',') .join(',')
} }




+ 1
- 1
package.json View File

@@ -1,6 +1,6 @@
{ {
"name": "@tesseract-design/react-common", "name": "@tesseract-design/react-common",
"version": "0.0.0",
"version": "0.0.1",
"description": "Common front-end components for Web using the Tesseract design system, written in React.", "description": "Common front-end components for Web using the Tesseract design system, written in React.",
"directories": { "directories": {
"lib": "dist" "lib": "dist"


Loading…
Cancel
Save