Skip to content

Commit

Permalink
feat: implements number input with PR comment feedback
Browse files Browse the repository at this point in the history
  • Loading branch information
jongomez committed Oct 4, 2023
1 parent a7bfa14 commit 4e09092
Show file tree
Hide file tree
Showing 7 changed files with 356 additions and 0 deletions.
3 changes: 3 additions & 0 deletions packages/lsd-react/src/components/CSSBaseline/CSSBaseline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ import { TagStyles } from '../Tag/Tag.styles'
import { TextFieldStyles } from '../TextField/TextField.styles'
import { defaultThemes, Theme, withTheme } from '../Theme'
import { TypographyStyles } from '../Typography/Typography.styles'
import { TableFooterStyles } from '../TableFooter/TableFooter.styles'
import { NumberInputStyles } from '../NumberInput/NumberInput.styles'

const componentStyles: Array<ReturnType<typeof withTheme> | SerializedStyles> =
[
Expand Down Expand Up @@ -66,6 +68,7 @@ const componentStyles: Array<ReturnType<typeof withTheme> | SerializedStyles> =
TableBodyStyles,
TableItemStyles,
TableRowStyles,
NumberInputStyles,
]

export const CSSBaseline: React.FC<{ theme?: Theme }> = ({
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
export const numberInputClasses = {
root: `lsd-number-input`,
label: 'lsd-number-input__label',

mainContainer: `lsd-number-input__main-container`,

inputContainer: `lsd-number-input__input-container`,
input: `lsd-number-input__input`,

errorIcon: `lsd-number-input__error-icon`,
plusMinusIcons: `lsd-number-input__plus-minus-icons`,

supportingText: 'lsd-number-input__supporting-text',

disabled: `lsd-number-input--disabled`,
error: 'lsd-number-input--error',

large: `lsd-number-input--large`,
medium: `lsd-number-input--medium`,
small: `lsd-number-input--small`,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { Meta, Story } from '@storybook/react'
import { NumberInput, NumberInputProps } from './NumberInput'

export default {
title: 'NumberInput',
component: NumberInput,
argTypes: {
size: {
type: {
name: 'enum',
value: ['small', 'medium', 'large'],
},
defaultValue: 'large',
},
},
} as Meta

export const Root: Story<NumberInputProps> = ({ ...args }) => {
return <NumberInput {...args} />
}

Root.args = {
id: 'label',
size: 'large',
supportingText: 'Supporting text',
disabled: false,
value: undefined,
onChange: undefined,
error: false,
errorIcon: false,
min: -10,
max: 10,
step: 1,
label: 'Label',
}
137 changes: 137 additions & 0 deletions packages/lsd-react/src/components/NumberInput/NumberInput.styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import { css } from '@emotion/react'
import { numberInputClasses } from './NumberInput.classes'

export const NumberInputStyles = css`
.${numberInputClasses.root} {
width: auto;
box-sizing: border-box;
}
.${numberInputClasses.mainContainer}:hover {
text-decoration: underline;
}
.${numberInputClasses.error} {
.${numberInputClasses.mainContainer} {
text-decoration: line-through;
}
}
.${numberInputClasses.label} {
display: block;
}
.${numberInputClasses.plusMinusIcons} {
display: flex;
flex-shrink: 0;
}
.${numberInputClasses.inputContainer} {
box-sizing: border-box;
border: 1px solid rgb(var(--lsd-border-primary));
border-left: 0px;
border-right: 0px;
}
.${numberInputClasses.errorIcon} {
cursor: pointer;
display: flex;
align-items: center;
padding: 10px 8px;
}
.${numberInputClasses.inputContainer} {
display: flex;
align-items: center;
justify-content: space-between;
}
.${numberInputClasses.disabled} {
opacity: 0.34;
}
.${numberInputClasses.mainContainer} {
display: flex;
align-items: center;
}
.${numberInputClasses.input} {
border: none;
outline: none;
font-size: 14px;
color: rgb(var(--lsd-text-primary));
background: none;
text-align: center;
padding: 0 4px;
}
.${numberInputClasses.input}::-webkit-inner-spin-button {
display: none;
-webkit-appearance: none;
}
.${numberInputClasses.input}:hover {
outline: none;
}
.${numberInputClasses.supportingText} {
position: absolute;
}
.${numberInputClasses.large} {
.${numberInputClasses.label} {
margin: 0 0 6px 18px;
}
.${numberInputClasses.inputContainer} {
height: 40px;
}
.${numberInputClasses.input} {
width: 62px;
}
.${numberInputClasses.plusMinusIcons} {
height: 40px;
width: 40px;
}
.${numberInputClasses.supportingText} {
margin: 6px 18px 0 18px;
}
}
.${numberInputClasses.medium} {
.${numberInputClasses.label} {
margin: 0 0 6px 14px;
}
.${numberInputClasses.inputContainer} {
height: 32px;
}
.${numberInputClasses.input} {
width: 58px;
}
.${numberInputClasses.plusMinusIcons} {
height: 32px;
width: 32px;
}
.${numberInputClasses.supportingText} {
margin: 6px 14px 0 14px;
}
}
.${numberInputClasses.small} {
.${numberInputClasses.label} {
margin: 0 0 6px 12px;
}
.${numberInputClasses.inputContainer} {
height: 28px;
}
.${numberInputClasses.input} {
width: 50px;
}
.${numberInputClasses.plusMinusIcons} {
height: 28px;
width: 28px;
}
.${numberInputClasses.supportingText} {
margin: 6px 12px 0 12px;
}
}
`
158 changes: 158 additions & 0 deletions packages/lsd-react/src/components/NumberInput/NumberInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import clsx from 'clsx'
import React, { useRef } from 'react'
import { useInput } from '../../utils/useInput'
import { AddIcon, ErrorIcon, RemoveIcon } from '../Icons'
import { Typography } from '../Typography'
import { numberInputClasses } from './NumberInput.classes'
import { IconButton } from '../IconButton'
import {
CommonProps,
omitCommonProps,
useCommonProps,
} from '../../utils/useCommonProps'

export type NumberInputProps = CommonProps &
Omit<React.HTMLAttributes<HTMLDivElement>, 'onChange' | 'value'> &
Pick<React.InputHTMLAttributes<HTMLInputElement>, 'onChange'> & {
label?: React.ReactNode
size?: 'large' | 'medium' | 'small'
error?: boolean
errorIcon?: boolean
disabled?: boolean
supportingText?: string
value?: string
defaultValue?: string
placeholder?: string
icon?: React.ReactNode
min?: number
max?: number
step?: number
inputProps?: React.InputHTMLAttributes<HTMLInputElement>
}

export const NumberInput: React.FC<NumberInputProps> & {
classes: typeof numberInputClasses
} = ({
label,
size = 'large',
error = false,
errorIcon = false,
supportingText,
value,
placeholder,
defaultValue,
disabled,
onChange,
icon,
inputProps = {},
id = 'number-input',
min = Number.MIN_SAFE_INTEGER,
max = Number.MAX_SAFE_INTEGER,
step = 1,
...props
}) => {
const ref = useRef<HTMLInputElement>(null)
const commonProps = useCommonProps(props)
const input = useInput({ defaultValue, value, onChange, ref })

const handleIncrement = () => {
if (disabled) return
const newValue = Math.min(max, Number(input.value || '0') + step)
input.setValue(newValue.toString())

const fakeEvent = {
target: { value: newValue.toString() },
}

if (onChange) {
input.onChange(fakeEvent as any)
}
}

const handleDecrement = () => {
if (disabled) return
const newValue = Math.max(min, Number(input.value || '0') - step)
input.setValue(newValue.toString())

const fakeEvent = {
target: { value: newValue.toString() },
}

if (onChange) {
input.onChange(fakeEvent as any)
}
}

return (
<div
aria-disabled={disabled ? 'true' : 'false'}
{...omitCommonProps(props)}
className={clsx(
props.className,
commonProps.className,
numberInputClasses.root,
numberInputClasses[size],
disabled && numberInputClasses.disabled,
error && numberInputClasses.error,
)}
>
{label && (
<Typography
htmlFor={id}
className={numberInputClasses.label}
variant="label2"
component="label"
>
{label}
</Typography>
)}

<div className={numberInputClasses.mainContainer}>
<IconButton
onClick={handleDecrement}
className={numberInputClasses.plusMinusIcons}
>
<RemoveIcon color="primary" />
</IconButton>

<div className={numberInputClasses.inputContainer}>
<input
id={id}
type="number"
placeholder={placeholder}
ref={ref}
className={clsx(inputProps.className, numberInputClasses.input)}
value={input.value || ''}
onChange={input.onChange}
min={min}
max={max}
step={step}
disabled={disabled}
{...inputProps}
/>
{error && !!errorIcon && (
<span className={numberInputClasses.errorIcon}>
<ErrorIcon color="primary" />
</span>
)}
</div>

<IconButton
onClick={handleIncrement}
className={numberInputClasses.plusMinusIcons}
>
<AddIcon color="primary" />
</IconButton>
</div>
{supportingText && (
<div className={clsx(numberInputClasses.supportingText)}>
<Typography variant={'label2'} component="p">
{supportingText}
</Typography>
</div>
)}
</div>
)
}

NumberInput.classes = numberInputClasses
1 change: 1 addition & 0 deletions packages/lsd-react/src/components/NumberInput/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './NumberInput'
1 change: 1 addition & 0 deletions packages/lsd-react/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,4 @@ export * from './components/Tag'
export * from './components/TextField'
export * from './components/Theme'
export * from './components/Typography'
export * from './components/NumberInput'

0 comments on commit 4e09092

Please sign in to comment.