Cover scenarios primarily for adjusted behavior on Button, as well as ensure components are exported correctly.tags/0.3.0
@@ -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 | ||||
@@ -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" | ||||
} | } |
@@ -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 | ||||
@@ -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 | |||||
}, | }, | ||||
) | ) | ||||
@@ -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' | ||||
@@ -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, | ||||
@@ -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' | ||||
@@ -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', | ||||
@@ -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' | ||||
@@ -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 | ||||
@@ -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 />) | ||||
@@ -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, | ||||
@@ -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' | ||||
@@ -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, | ||||
@@ -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', () => { | ||||
@@ -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 | ||||
@@ -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() | |||||
}) | |||||
}) |
@@ -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) | |||||
}), | |||||
) | ) | ||||
}) | }) | ||||
}) | }) | ||||
@@ -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 |
@@ -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', | |||||
}) | |||||
}), | |||||
) | ) | ||||
}) | }) |
@@ -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') | ||||
} | } | ||||
@@ -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(',') | |||||
}), | |||||
) | ) | ||||
}) | }) | ||||
}) | }) | ||||
@@ -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,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" | ||||