Parcourir la 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 il y a 4 ans
Parent
révision
f83cd94cda
24 fichiers modifiés avec 534 ajouts et 520 suppressions
  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 Voir le fichier

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


+ 1
- 1
.prettierrc Voir le fichier

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

+ 51
- 13
lib/components/Button/Button.test.tsx Voir le fichier

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

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

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

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

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

it('should exist', () => {
expect(Button).toBeDefined()
})
@@ -23,21 +24,46 @@ it('should render without crashing given minimum required props', () => {
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', () => {
@@ -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) => {
const variant = rawVariant as Variant

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

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

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


+ 147
- 71
lib/components/Button/Button.tsx Voir le fichier

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

export type Variant = 'outline' | 'primary'

export type ButtonElement = 'a' | 'button'

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

const MIN_HEIGHTS: SizeMap<string | number> = {
small: '2.5rem',
medium: '3rem',
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': {
'--color-accent': 'var(--color-active, Highlight)',
'outline': 0,
},
':disabled': {
opacity: 0.5,
cursor: 'not-allowed',
outline: 0,
},
':disabled': disabledButtonStyles,
'::-moz-focus-inner': {
outline: 0,
border: 0,
},
})
}

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

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

Base.displayName = 'button'

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

LinkBase.displayName = 'a'

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

DisabledLinkBase.displayName = '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': {
position: 'absolute',
top: 0,
@@ -77,6 +99,19 @@ const Border = styled('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 = {
/**
* Size of the component.
@@ -85,11 +120,7 @@ const propTypes = {
/**
* 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.
*/
@@ -98,43 +129,88 @@ const propTypes = {
* Can the component be activated?
*/
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>

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

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 />
{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 Voir le fichier

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

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


+ 13
- 13
lib/components/Checkbox/Checkbox.tsx Voir le fichier

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

const CaptureArea = styled('label')({
'marginTop': '0.25rem',
marginTop: '0.25rem',
'::after': {
content: '""',
},
@@ -52,18 +52,18 @@ const IndicatorWrapper = 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': {
position: 'absolute',
top: 0,


+ 3
- 0
lib/components/Icon/Icon.test.tsx Voir le fichier

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

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


+ 4
- 17
lib/components/Icon/Icon.tsx Voir le fichier

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

const Label = styled('span')({
'position': 'absolute',
'left': -999999,
'width': 1,
'height': 1,
position: 'absolute',
left: -999999,
width: 1,
height: 1,
':empty': {
display: 'none',
},
@@ -51,19 +51,6 @@ const 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> = ({
name,
weight = '0.125rem',


+ 3
- 0
lib/components/RadioButton/RadioButton.test.tsx Voir le fichier

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

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


+ 33
- 28
lib/components/RadioButton/RadioButton.tsx Voir le fichier

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

const CaptureArea = styled('label')({
'marginTop': '0.25rem',
marginTop: '0.25rem',
'::after': {
content: '""',
clear: 'both',
@@ -52,18 +52,18 @@ const IndicatorWrapper = 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': {
position: 'absolute',
top: 0,
@@ -131,21 +131,26 @@ type Props = PropTypes.InferProps<typeof propTypes>
* @see {@link Select} for a similar component suitable for selecting more values.
* @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



+ 3
- 27
lib/components/Select/Select.test.tsx Voir le fichier

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

import * as fc from 'fast-check'
import * as Enzyme from 'enzyme'
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', () => {
const wrapper = Enzyme.shallow(<Select disabled />)



+ 35
- 51
lib/components/Select/Select.tsx Voir le fichier

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

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': {
'--color-accent': 'var(--color-active, Highlight)',
},
@@ -76,21 +76,22 @@ const LabelWrapper = styled('span')({
LabelWrapper.displayName = 'span'

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': {
outline: 0,
},
@@ -106,20 +107,20 @@ const Input = styled('select')({
Input.displayName = 'select'

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': {
position: 'absolute',
top: 0,
@@ -188,10 +189,6 @@ const propTypes = {
* Size of the component.
*/
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?
*/
@@ -208,22 +205,13 @@ const 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>(
(
{
label = '',
className = '',
hint = '',
size: sizeProp = 'medium',
block = false,
size = 'medium',
multiple = false,
disabled = false,
style = {},
@@ -231,11 +219,9 @@ const Select = React.forwardRef<HTMLSelectElement, Props>(
},
ref,
) => {
const size = sizeProp as Size
return (
<ComponentBase
style={{
display: block ? 'block' : 'inline-block',
opacity: disabled ? 0.5 : undefined,
}}
>
@@ -259,10 +245,8 @@ const Select = React.forwardRef<HTMLSelectElement, Props>(
multiple={multiple!}
style={{
...style,
display: block ? 'block' : 'inline-block',
verticalAlign: 'top',
fontSize: INPUT_FONT_SIZES[size!],
width: block || multiple ? '100%' : undefined,
height: multiple ? undefined : MIN_HEIGHTS[size!],
minHeight: MIN_HEIGHTS[size!],
resize: multiple ? 'vertical' : undefined,


+ 3
- 0
lib/components/Slider/Slider.test.tsx Voir le fichier

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

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


+ 37
- 37
lib/components/Slider/Slider.tsx Voir le fichier

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

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

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

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

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': {
position: 'absolute',
top: 0,
@@ -113,12 +113,12 @@ const LabelWrapper = styled('span')({
LabelWrapper.displayName = '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': {
display: 'block',
content: "''",
@@ -131,17 +131,17 @@ const FallbackTrack = styled('span')({
})

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': {
outline: 0,
border: 0,


+ 7
- 38
lib/components/TextInput/TextInput.test.tsx Voir le fichier

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

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

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 multiline = rawMultiline as boolean
const inputWidth = rawInputWidth as string

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


+ 118
- 135
lib/components/TextInput/TextInput.tsx Voir le fichier

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

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

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': {
'--color-accent': 'var(--color-active, Highlight)',
},
@@ -78,20 +77,20 @@ const LabelWrapper = styled('span')({
LabelWrapper.displayName = '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': {
position: 'absolute',
top: 0,
@@ -109,23 +108,24 @@ const Border = styled('span')({
})

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': {
outline: 0,
color: 'var(--color-fg, black)',
@@ -138,22 +138,23 @@ const Input = styled('input')({
Input.displayName = 'input'

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': {
outline: 0,
},
@@ -216,10 +217,6 @@ const propTypes = {
* Additional description, usually graphical, indicating the nature of the component's value.
*/
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?
*/
@@ -240,12 +237,6 @@ const 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>(
(
{
@@ -253,92 +244,84 @@ const TextInput = React.forwardRef<HTMLInputElement | HTMLTextAreaElement, Props
className = '',
hint = '',
indicator = null,
size: sizeProp = 'medium',
block = false,
size = 'medium',
multiline = false,
disabled = false,
autoResize = false,
placeholder = '',
},
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={{
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',
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={{
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


+ 26
- 0
lib/index.test.ts Voir le fichier

@@ -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 Voir le fichier

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

it('should return a boolean value', () => {
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`', () => {
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 Voir le fichier

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

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

export default isEmpty

+ 14
- 27
lib/services/splitValueAndUnit.test.ts Voir le fichier

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

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

it('should throw a TypeError when invalid values are supplied', () => {
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', () => {
fc.assert(
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({
magnitude,
unit,
})
}
)
},
),
)
})

it('should parse numbers as CSS numbers with implicit pixel units', () => {
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 Voir le fichier

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

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

interface SplitValueAndUnit {
(value: any): ValueAndUnit
}

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


+ 18
- 37
lib/services/stringify.test.ts Voir le fichier

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

it('should return a string value', () => {
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', () => {
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', () => {
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', () => {
it('should stringify empty arrays', () => {
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', () => {
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', () => {
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 Voir le fichier

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

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

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

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



+ 1
- 1
package.json Voir le fichier

@@ -1,6 +1,6 @@
{
"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.",
"directories": {
"lib": "dist"


Chargement…
Annuler
Enregistrer