Skip to content

Commit

Permalink
feat: add input number
Browse files Browse the repository at this point in the history
  • Loading branch information
NateWaldschmidt committed Feb 14, 2024
1 parent 858f7f6 commit b0efbb2
Show file tree
Hide file tree
Showing 10 changed files with 332 additions and 4 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# CHANGELOG

## v2.0.12

- Deprecate the `CurrencyInput` component
- Add the `InputNumber` component

## v2.0.11

- Update linting to understand Vue globals
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

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

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@lob/ui-components",
"version": "2.0.11",
"version": "2.0.12",
"engines": {
"node": ">=20.2.0",
"npm": ">=10.2.0"
Expand Down
4 changes: 3 additions & 1 deletion src/components/CurrencyInput/CurrencyInput.mdx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { Canvas, Story, ArgTypes, PRIMARY_STORY } from '@storybook/addon-docs';
import { Canvas, ArgTypes, PRIMARY_STORY } from '@storybook/addon-docs';
import { Primary } from './CurrencyInput.stories';

**NOTE:** This component is deprecated in favor of `InputNumber`.

# Currency Input

A currency input component for billing form workflows.
Expand Down
16 changes: 16 additions & 0 deletions src/components/InputNumber/InputNumber.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Canvas, ArgTypes, PRIMARY_STORY } from '@storybook/addon-docs';
import { Primary, Currency } from './InputNumber.stories';

# Input Number

## Primary

<Canvas of={Primary} />

## Currency

<Canvas of={Currency} />

## Props

<ArgTypes story={PRIMARY_STORY} />
50 changes: 50 additions & 0 deletions src/components/InputNumber/InputNumber.stories.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import InputNumber from './InputNumber.vue';
import mdx from './InputNumber.mdx';

export default {
title: 'Components/Input Number',
component: InputNumber,
parameters: {
docs: {
page: mdx
}
}
};

const inputNumberModel = 5000;

const PrimaryTemplate = (args, { argTypes }) => ({
props: Object.keys(argTypes),
components: { InputNumber },
setup: () => ({ args }),
data: () => ({ inputNumberModel }),
template: '<InputNumber v-bind="args" v-model="inputNumberModel" />'
});

export const Primary = PrimaryTemplate.bind({});
Primary.args = {
id: 'input-number',
name: 'input-number',
label: 'Input Number',
helperText: 'Helper text',
placeholder: 'Amount'
};

const inputCurrencyModel = 5000;

const CurrencyTemplate = (args, { argTypes }) => ({
props: Object.keys(argTypes),
components: { InputNumber },
setup: () => ({ args }),
data: () => ({ inputCurrencyModel }),
template: '<InputNumber v-bind="args" v-model="inputCurrencyModel" />'
});

export const Currency = CurrencyTemplate.bind({});
Currency.args = {
id: 'input-currency',
name: 'input-currency',
label: 'Input Currency',
placeholder: 'Amount',
mode: 'currency'
};
191 changes: 191 additions & 0 deletions src/components/InputNumber/InputNumber.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
<template>
<div
class="uic-input-number-container"
data-testid="uic-input-number-container"
>
<Label
:label-for="id"
:label="label"
:required="required"
:sr-only-label="hideLabel"
data-testid="uic-input-number-label"
/>
<InputNumber
:currency="currency"
:disabled="disabled"
:input-class="[
'uic-input-number',
{ 'uic-error': error },
{ 'uic-success': success }
]"
:input-id="id"
:input-props="computedInputProps"
:invalid="error"
:max="max"
:min-fraction-digits="minFractionDigits"
:min="min"
:mode="mode"
:model-value="modelValue"
:placeholder="placeholder"
:readonly="readonly"
locale="en-US"
unstyled
@blur="emits('blur')"
@focus="emits('focus')"
@input="(e) => emits('update:modelValue', e.value as number)"
/>
<!-- The PrimeVue number input does not have helper text, I copied this from the PrimeVue text input. -->
<small
v-if="helperText"
:id="helperTextId"
:class="[
'uic-input-number-helper',
{ 'uic-error': error },
{ 'uic-success': success }
]"
data-testid="uic-input-number-helper"
>
{{ helperText }}
</small>
</div>
</template>

<script lang="ts">
export default {
inheritAttrs: false
};
</script>
<script setup lang="ts">
import InputNumber, { InputNumberProps } from 'primevue/inputnumber';
import { InputHTMLAttributes, computed, useAttrs } from 'vue';
import Label from '../Label/Label.vue';
import { InputNumberMode } from './constants';
const attrs = useAttrs();
const props = withDefaults(
defineProps<{
disabled?: InputNumberProps['disabled'];
error?: boolean;
helperText?: string;
hideLabel?: boolean;
id: InputNumberProps['inputId'];
label: string;
max?: InputNumberProps['max'];
maxFractionDigits?: InputNumberProps['maxFractionDigits'];
min?: InputNumberProps['min'];
minFractionDigits?: InputNumberProps['minFractionDigits'];
mode?: InputNumberMode;
modelValue?: number;
name: string;
placeholder?: InputNumberProps['placeholder'];
readonly?: InputNumberProps['readonly'];
required?: boolean;
success?: boolean;
}>(),
{
disabled: false,
error: false,
helperText: undefined,
hideLabel: false,
max: undefined,
maxFractionDigits: undefined,
min: undefined,
minFractionDigits: undefined,
mode: InputNumberMode.DECIMAL,
modelValue: 0,
placeholder: undefined,
readonly: false,
required: false,
success: false
}
);
const emits = defineEmits<{
(e: 'update:modelValue', value: number): void;
(e: 'change', value: number): void;
(e: 'focus'): void;
(e: 'blur'): void;
}>();
const helperTextId = computed(() => `${props.id}-helper`);
const computedInputProps = computed<InputHTMLAttributes>(() => {
return {
...attrs,
'aria-describedby': props.helperText ? helperTextId.value : undefined,
name: props.name,
required: props.required
};
});
const currency = computed(() =>
props.mode === InputNumberMode.CURRENCY ? 'USD' : undefined
);
</script>

<style lang="scss">
.uic-input-number-container {
@apply flex flex-col gap-1;
}
.uic-input-number {
@apply px-4 py-3;
@apply text-sm text-gray-800;
@apply bg-white;
@apply border border-gray-200 rounded;
outline: none;
&:hover {
@apply border-gray-300;
}
&:focus-within {
@apply border-gray-400;
}
&::placeholder {
@apply text-gray-200;
}
&.uic-error {
@apply bg-red-50;
@apply border-red-600;
@apply text-red-600;
&::placeholder {
@apply text-red-600;
}
}
&.uic-success {
@apply bg-green-50;
@apply border-green-700;
@apply text-green-700;
&::placeholder {
@apply text-green-700;
}
}
&:disabled {
@apply bg-gray-50;
@apply text-gray-300;
@apply border-gray-200;
&::placeholder {
@apply text-gray-300;
}
}
}
.uic-input-number-helper {
@apply text-xs text-gray-500;
&.uic-error {
@apply text-red-600;
}
&.uic-success {
@apply text-green-700;
}
}
</style>
57 changes: 57 additions & 0 deletions src/components/InputNumber/__tests__/InputNumber.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import '@testing-library/jest-dom';
import { fireEvent, render } from '@testing-library/vue';
import InputNumber from '../InputNumber.vue';
import { ExtractPropTypes } from 'vue';

describe('InputNumber', () => {
const DEFAULT_PROPS: ExtractPropTypes<typeof InputNumber> = {
label: 'Test Input Number',
id: 'test-input-number',
name: 'test',
'data-testid': 'uic-input-number',
helperText: 'Test helper text'
};

it('renders', () => {
const { getByTestId } = render(InputNumber, { props: DEFAULT_PROPS });
expect(getByTestId('uic-input-number-container')).toBeVisible();
const label = getByTestId('uic-input-number-label');
expect(label).toBeVisible();
expect(label.textContent).toContain(DEFAULT_PROPS.label);
expect(getByTestId('uic-input-number')).toBeVisible();
const helperText = getByTestId('uic-input-number-helper');
expect(helperText).toBeVisible();
expect(helperText.textContent).toContain(DEFAULT_PROPS.helperText);
});

it('updates', () => {
const { getByTestId } = render(InputNumber, {
props: { ...DEFAULT_PROPS, modelValue: 50 }
});
const numberInput = getByTestId('uic-input-number');

expect(numberInput).toHaveValue('50');
fireEvent.update(numberInput, '123');
expect(numberInput).toHaveValue('123');
});

it('emits focus', () => {
const { getByTestId, emitted } = render(InputNumber, {
props: DEFAULT_PROPS
});
const numberInput = getByTestId('uic-input-number');

fireEvent.focus(numberInput);
expect(emitted()).toHaveProperty('focus');
});

it('emits blur', () => {
const { getByTestId, emitted } = render(InputNumber, {
props: DEFAULT_PROPS
});
const numberInput = getByTestId('uic-input-number');

fireEvent.blur(numberInput);
expect(emitted()).toHaveProperty('blur');
});
});
6 changes: 6 additions & 0 deletions src/components/InputNumber/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export const InputNumberMode = {
CURRENCY: 'currency',
DECIMAL: 'decimal'
} as const;
export type InputNumberMode =
(typeof InputNumberMode)[keyof typeof InputNumberMode];
1 change: 1 addition & 0 deletions src/components/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export { default as FileUpload } from './FileUpload/FileUpload';
export { default as FilterContent } from './FilterContent/FilterContent';
export { default as Icon } from './Icon/Icon.vue';
export * from './Icons';
export { default as InputNumber } from './InputNumber/InputNumber.vue';
export { default as LegacyModal } from './LegacyModal/LegacyModal.vue';
export { default as LoadingIndicator } from './LoadingIndicator/LoadingIndicator';
export { default as LobLink } from './Link/Link';
Expand Down

0 comments on commit b0efbb2

Please sign in to comment.