Skip to content
This repository was archived by the owner on Mar 26, 2025. It is now read-only.

Commit 755ebe1

Browse files
feat: add input number
1 parent 858f7f6 commit 755ebe1

File tree

10 files changed

+366
-4
lines changed

10 files changed

+366
-4
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# CHANGELOG
22

3+
## v2.0.12
4+
5+
- Deprecate the `CurrencyInput` component
6+
- Add the `InputNumber` component
7+
38
## v2.0.11
49

510
- Update linting to understand Vue globals

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@lob/ui-components",
3-
"version": "2.0.11",
3+
"version": "2.0.12",
44
"engines": {
55
"node": ">=20.2.0",
66
"npm": ">=10.2.0"

src/components/CurrencyInput/CurrencyInput.mdx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1-
import { Canvas, Story, ArgTypes, PRIMARY_STORY } from '@storybook/addon-docs';
1+
import { Canvas, ArgTypes, PRIMARY_STORY } from '@storybook/addon-docs';
22
import { Primary } from './CurrencyInput.stories';
33

4+
**NOTE:** This component is deprecated in favor of `InputNumber`.
5+
46
# Currency Input
57

68
A currency input component for billing form workflows.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { Canvas, ArgTypes, PRIMARY_STORY } from '@storybook/addon-docs';
2+
import { Primary, Currency } from './InputNumber.stories';
3+
4+
# Input Number
5+
6+
## Primary
7+
8+
<Canvas of={Primary} />
9+
10+
## Currency
11+
12+
<Canvas of={Currency} />
13+
14+
## Props
15+
16+
<ArgTypes story={PRIMARY_STORY} />
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import InputNumber from './InputNumber.vue';
2+
import mdx from './InputNumber.mdx';
3+
import { InputNumberMode } from './constants';
4+
5+
export default {
6+
title: 'Components/Input Number',
7+
component: InputNumber,
8+
parameters: {
9+
docs: {
10+
page: mdx
11+
}
12+
},
13+
argTypes: {
14+
disabled: {
15+
control: 'boolean'
16+
},
17+
id: {
18+
control: 'text'
19+
},
20+
max: {
21+
control: 'number'
22+
},
23+
maxFractionDigits: {
24+
control: 'number'
25+
},
26+
min: {
27+
control: 'number'
28+
},
29+
minFractionDigits: {
30+
control: 'number'
31+
},
32+
mode: {
33+
options: Object.values(InputNumberMode),
34+
control: {
35+
type: 'select'
36+
}
37+
},
38+
placeholder: {
39+
control: 'text'
40+
},
41+
readonly: {
42+
control: 'boolean'
43+
}
44+
}
45+
};
46+
47+
const inputNumberModel = 5000;
48+
49+
const PrimaryTemplate = (args, { argTypes }) => ({
50+
props: Object.keys(argTypes),
51+
components: { InputNumber },
52+
setup: () => ({ args }),
53+
data: () => ({ inputNumberModel }),
54+
template: '<InputNumber v-bind="args" v-model="inputNumberModel" />'
55+
});
56+
57+
export const Primary = PrimaryTemplate.bind({});
58+
Primary.args = {
59+
id: 'input-number',
60+
name: 'input-number',
61+
label: 'Input Number',
62+
helperText: 'Helper text',
63+
placeholder: 'Amount',
64+
mode: InputNumberMode.DECIMAL
65+
};
66+
67+
const inputCurrencyModel = 5000;
68+
69+
const CurrencyTemplate = (args, { argTypes }) => ({
70+
props: Object.keys(argTypes),
71+
components: { InputNumber },
72+
setup: () => ({ args }),
73+
data: () => ({ inputCurrencyModel }),
74+
template: '<InputNumber v-bind="args" v-model="inputCurrencyModel" />'
75+
});
76+
77+
export const Currency = CurrencyTemplate.bind({});
78+
Currency.args = {
79+
id: 'input-currency',
80+
name: 'input-currency',
81+
label: 'Input Currency',
82+
placeholder: 'Amount',
83+
mode: InputNumberMode.CURRENCY
84+
};
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
<template>
2+
<div
3+
class="uic-input-number-container"
4+
data-testid="uic-input-number-container"
5+
>
6+
<Label
7+
:label-for="id"
8+
:label="label"
9+
:required="required"
10+
:sr-only-label="hideLabel"
11+
data-testid="uic-input-number-label"
12+
/>
13+
<InputNumber
14+
:currency="currency"
15+
:disabled="disabled"
16+
:input-class="[
17+
'uic-input-number',
18+
{ 'uic-error': error },
19+
{ 'uic-success': success }
20+
]"
21+
:input-id="id"
22+
:input-props="computedInputProps"
23+
:invalid="error"
24+
:max="max"
25+
:min-fraction-digits="minFractionDigits"
26+
:min="min"
27+
:mode="mode"
28+
:model-value="modelValue"
29+
:placeholder="placeholder"
30+
:readonly="readonly"
31+
locale="en-US"
32+
unstyled
33+
@blur="emits('blur')"
34+
@focus="emits('focus')"
35+
@input="(e) => emits('update:modelValue', e.value as number)"
36+
/>
37+
<!-- The PrimeVue number input does not have helper text, I copied this from the PrimeVue text input. -->
38+
<small
39+
v-if="helperText"
40+
:id="helperTextId"
41+
:class="[
42+
'uic-input-number-helper',
43+
{ 'uic-error': error },
44+
{ 'uic-success': success }
45+
]"
46+
data-testid="uic-input-number-helper"
47+
>
48+
{{ helperText }}
49+
</small>
50+
</div>
51+
</template>
52+
53+
<script lang="ts">
54+
export default {
55+
inheritAttrs: false
56+
};
57+
</script>
58+
<script setup lang="ts">
59+
import InputNumber, { InputNumberProps } from 'primevue/inputnumber';
60+
import { InputHTMLAttributes, computed, useAttrs } from 'vue';
61+
62+
import Label from '../Label/Label.vue';
63+
import { InputNumberMode } from './constants';
64+
65+
const attrs = useAttrs();
66+
67+
const props = withDefaults(
68+
defineProps<{
69+
disabled?: InputNumberProps['disabled'];
70+
error?: boolean;
71+
helperText?: string;
72+
hideLabel?: boolean;
73+
id: InputNumberProps['inputId'];
74+
label: string;
75+
max?: InputNumberProps['max'];
76+
maxFractionDigits?: InputNumberProps['maxFractionDigits'];
77+
min?: InputNumberProps['min'];
78+
minFractionDigits?: InputNumberProps['minFractionDigits'];
79+
mode?: InputNumberMode;
80+
modelValue?: number;
81+
name: string;
82+
placeholder?: InputNumberProps['placeholder'];
83+
readonly?: InputNumberProps['readonly'];
84+
required?: boolean;
85+
success?: boolean;
86+
}>(),
87+
{
88+
disabled: false,
89+
error: false,
90+
helperText: undefined,
91+
hideLabel: false,
92+
max: undefined,
93+
maxFractionDigits: undefined,
94+
min: undefined,
95+
minFractionDigits: undefined,
96+
mode: InputNumberMode.DECIMAL,
97+
modelValue: 0,
98+
placeholder: undefined,
99+
readonly: false,
100+
required: false,
101+
success: false
102+
}
103+
);
104+
105+
const emits = defineEmits<{
106+
(e: 'update:modelValue', value: number): void;
107+
(e: 'change', value: number): void;
108+
(e: 'focus'): void;
109+
(e: 'blur'): void;
110+
}>();
111+
112+
const helperTextId = computed(() => `${props.id}-helper`);
113+
const computedInputProps = computed<InputHTMLAttributes>(() => {
114+
return {
115+
...attrs,
116+
'aria-describedby': props.helperText ? helperTextId.value : undefined,
117+
name: props.name,
118+
required: props.required
119+
};
120+
});
121+
const currency = computed(() =>
122+
props.mode === InputNumberMode.CURRENCY ? 'USD' : undefined
123+
);
124+
</script>
125+
126+
<style lang="scss">
127+
.uic-input-number-container {
128+
@apply flex flex-col gap-1;
129+
}
130+
131+
.uic-input-number {
132+
@apply px-4 py-3;
133+
@apply text-sm text-gray-800;
134+
@apply bg-white;
135+
@apply border border-gray-200 rounded;
136+
outline: none;
137+
138+
&:hover {
139+
@apply border-gray-300;
140+
}
141+
142+
&:focus-within {
143+
@apply border-gray-400;
144+
}
145+
146+
&::placeholder {
147+
@apply text-gray-200;
148+
}
149+
150+
&.uic-error {
151+
@apply bg-red-50;
152+
@apply border-red-600;
153+
@apply text-red-600;
154+
155+
&::placeholder {
156+
@apply text-red-600;
157+
}
158+
}
159+
160+
&.uic-success {
161+
@apply bg-green-50;
162+
@apply border-green-700;
163+
@apply text-green-700;
164+
165+
&::placeholder {
166+
@apply text-green-700;
167+
}
168+
}
169+
170+
&:disabled {
171+
@apply bg-gray-50;
172+
@apply text-gray-300;
173+
@apply border-gray-200;
174+
175+
&::placeholder {
176+
@apply text-gray-300;
177+
}
178+
}
179+
}
180+
.uic-input-number-helper {
181+
@apply text-xs text-gray-500;
182+
183+
&.uic-error {
184+
@apply text-red-600;
185+
}
186+
187+
&.uic-success {
188+
@apply text-green-700;
189+
}
190+
}
191+
</style>
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import '@testing-library/jest-dom';
2+
import { fireEvent, render } from '@testing-library/vue';
3+
import InputNumber from '../InputNumber.vue';
4+
import { ExtractPropTypes } from 'vue';
5+
6+
describe('InputNumber', () => {
7+
const DEFAULT_PROPS: ExtractPropTypes<typeof InputNumber> = {
8+
label: 'Test Input Number',
9+
id: 'test-input-number',
10+
name: 'test',
11+
'data-testid': 'uic-input-number',
12+
helperText: 'Test helper text'
13+
};
14+
15+
it('renders', () => {
16+
const { getByTestId } = render(InputNumber, { props: DEFAULT_PROPS });
17+
expect(getByTestId('uic-input-number-container')).toBeVisible();
18+
const label = getByTestId('uic-input-number-label');
19+
expect(label).toBeVisible();
20+
expect(label.textContent).toContain(DEFAULT_PROPS.label);
21+
expect(getByTestId('uic-input-number')).toBeVisible();
22+
const helperText = getByTestId('uic-input-number-helper');
23+
expect(helperText).toBeVisible();
24+
expect(helperText.textContent).toContain(DEFAULT_PROPS.helperText);
25+
});
26+
27+
it('updates', () => {
28+
const { getByTestId } = render(InputNumber, {
29+
props: { ...DEFAULT_PROPS, modelValue: 50 }
30+
});
31+
const numberInput = getByTestId('uic-input-number');
32+
33+
expect(numberInput).toHaveValue('50');
34+
fireEvent.update(numberInput, '123');
35+
expect(numberInput).toHaveValue('123');
36+
});
37+
38+
it('emits focus', () => {
39+
const { getByTestId, emitted } = render(InputNumber, {
40+
props: DEFAULT_PROPS
41+
});
42+
const numberInput = getByTestId('uic-input-number');
43+
44+
fireEvent.focus(numberInput);
45+
expect(emitted()).toHaveProperty('focus');
46+
});
47+
48+
it('emits blur', () => {
49+
const { getByTestId, emitted } = render(InputNumber, {
50+
props: DEFAULT_PROPS
51+
});
52+
const numberInput = getByTestId('uic-input-number');
53+
54+
fireEvent.blur(numberInput);
55+
expect(emitted()).toHaveProperty('blur');
56+
});
57+
});

0 commit comments

Comments
 (0)