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

Commit 48feccd

Browse files
SELF-302: Add KeyValueInput Component (#514)
* feat: add key value input component * feat: add key value input props
1 parent 0e52e93 commit 48feccd

File tree

10 files changed

+209
-3
lines changed

10 files changed

+209
-3
lines changed

CHANGELOG.md

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

3+
## v2.0.71
4+
5+
- Add `KeyValueInput` component
6+
37
## v2.0.70
48

59
- Add `EnvelopeSearch` and `ImageSearch` icons, and `CUSTOM_ENVELOPE` and `HTML_TEMPLATE` semantic icon entries.

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.70",
3+
"version": "2.0.71",
44
"engines": {
55
"node": ">=20.2.0",
66
"npm": ">=10.2.0"

src/assets/styles/main.scss

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,12 @@
7171
@apply cursor-not-allowed;
7272
}
7373
}
74+
75+
// TODO Move to `@layer components` when that works
76+
.uic-label {
77+
@apply type-small-500 text-gray-800;
78+
@apply mb-1;
79+
}
7480
}
7581

7682
// This is a hack for the modal styles flashing.
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { ArgTypes, Canvas, PRIMARY_STORY } from '@storybook/addon-docs';
2+
import { Primary } from './KeyValueInput.stories';
3+
4+
# KeyValueInput
5+
6+
<Canvas of={Primary} />
7+
8+
## Props
9+
10+
<ArgTypes story={PRIMARY_STORY} />
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { Meta, StoryObj } from '@storybook/vue3';
2+
import mdx from './KeyValueInput.mdx';
3+
// @ts-ignore No types from Vue file
4+
import KeyValueInput from './KeyValueInput.vue';
5+
6+
const meta: Meta<typeof KeyValueInput> = {
7+
title: 'Components/KeyValueInput',
8+
component: KeyValueInput,
9+
parameters: {
10+
docs: {
11+
page: mdx
12+
}
13+
}
14+
};
15+
16+
export default meta;
17+
18+
const keyValueModel: [string, string][] = [];
19+
20+
export const Primary: StoryObj<typeof KeyValueInput> = {
21+
args: {
22+
label: 'Key Value Input'
23+
},
24+
render: (args) => ({
25+
components: { KeyValueInput },
26+
data: () => ({ keyValueModel }),
27+
setup: () => ({ args }),
28+
template: `<KeyValueInput v-bind="args" v-model="keyValueModel" style="width: 30rem;" />`
29+
})
30+
};
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
<template>
2+
<div data-testid="uic-key-value-input">
3+
<fieldset class="w-full">
4+
<legend class="uic-label">{{ label }}</legend>
5+
<ul class="flex flex-col gap-4 w-full">
6+
<li
7+
v-for="([key, value], index) in modelValue"
8+
:key="`key-value-${index}`"
9+
class="grid gap-6 items-start"
10+
style="grid-template-columns: 1fr 1fr min-content"
11+
>
12+
<TextInput
13+
:id="`key-${index}`"
14+
:data-testid="`uic-key-value-input-key-${index}`"
15+
:disabled
16+
:error="Boolean(keyErrors?.[index])"
17+
:helper-text="keyErrors?.[index]"
18+
:label="keyLabel"
19+
:model-value="key"
20+
sr-only-label
21+
@update:modelValue="handleKeyChange(index, $event)"
22+
@blur="$emit('keyBlur', index)"
23+
@focus="$emit('keyFocus', index)"
24+
@input="$emit('keyInput', index)"
25+
/>
26+
<TextInput
27+
:id="`value-${index}`"
28+
:data-testid="`uic-key-value-input-value-${index}`"
29+
:disabled
30+
:error="Boolean(valueErrors?.[index])"
31+
:helper-text="valueErrors?.[index]"
32+
:label="valueLabel"
33+
:model-value="value"
34+
sr-only-label
35+
@update:modelValue="handleValueChange(index, $event)"
36+
@blur="$emit('valueBlur', index)"
37+
@focus="$emit('valueFocus', index)"
38+
@input="$emit('valueInput', index)"
39+
/>
40+
<IconButton
41+
:data-testid="`uic-key-value-input-delete-${index}`"
42+
:disabled
43+
icon="Delete"
44+
type="button"
45+
color="error"
46+
variant="outlined"
47+
@click="handleDelete(index)"
48+
/>
49+
</li>
50+
</ul>
51+
</fieldset>
52+
53+
<Button
54+
class="mt-2"
55+
data-testid="uic-key-value-add-button"
56+
size="medium"
57+
type="button"
58+
variant="ghost"
59+
@click="handleAddField"
60+
>
61+
<Icon icon="Plus" class="mr-1" size="sm" />
62+
{{ addLabel }}
63+
</Button>
64+
</div>
65+
</template>
66+
67+
<script setup lang="ts">
68+
import Button from '@/components/Button/Button.vue';
69+
import { Icon } from '@/components/Icon';
70+
import { IconButton } from '@/components/IconButton';
71+
import TextInput from '@/components/TextInput/TextInput.vue';
72+
73+
const props = withDefaults(
74+
defineProps<{
75+
addLabel?: string;
76+
disabled?: boolean;
77+
/** Errors correspond with the field's index. */
78+
keyErrors?: Record<number, string>;
79+
keyLabel?: string;
80+
label: string;
81+
/**
82+
* The `modelValue` is typed this way to allow invalid duplicated keys to be
83+
* shown with errors. To convert this to an object use
84+
* `Object.fromEntries(...)`
85+
*/
86+
modelValue: [string, string][];
87+
/** Errors correspond with the field's index. */
88+
valueErrors?: Record<number, string>;
89+
valueLabel?: string;
90+
}>(),
91+
{
92+
addLabel: 'Add field',
93+
disabled: false,
94+
keyErrors: undefined,
95+
keyLabel: 'Key',
96+
valueErrors: undefined,
97+
valueLabel: 'Value'
98+
}
99+
);
100+
101+
const emit = defineEmits<{
102+
(e: 'update:modelValue', value: [string, string][]): void; // eslint-disable-line no-unused-vars
103+
(e: 'keyFocus', index: number): void; // eslint-disable-line no-unused-vars
104+
(e: 'valueFocus', index: number): void; // eslint-disable-line no-unused-vars
105+
(e: 'keyBlur', index: number): void; // eslint-disable-line no-unused-vars
106+
(e: 'valueBlur', index: number): void; // eslint-disable-line no-unused-vars
107+
(e: 'keyInput', index: number): void; // eslint-disable-line no-unused-vars
108+
(e: 'valueInput', index: number): void; // eslint-disable-line no-unused-vars
109+
}>();
110+
111+
const handleKeyChange = (index: number, newKey: string) => {
112+
const newModelValue = [...props.modelValue];
113+
newModelValue[index][0] = newKey;
114+
115+
emit('update:modelValue', newModelValue);
116+
};
117+
118+
const handleValueChange = (index: number, newValue: string) => {
119+
const newModelValue = [...props.modelValue];
120+
newModelValue[index][1] = newValue;
121+
122+
emit('update:modelValue', newModelValue);
123+
};
124+
125+
const handleAddField = () => {
126+
emit('update:modelValue', [...props.modelValue, ['', '']]);
127+
};
128+
129+
const handleDelete = (index: number) => {
130+
const newModelValue = [...props.modelValue];
131+
newModelValue.splice(index, 1);
132+
133+
emit('update:modelValue', newModelValue);
134+
};
135+
</script>
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import '@testing-library/jest-dom';
2+
import { render } from '@testing-library/vue';
3+
import KeyValueInput from '../KeyValueInput.vue';
4+
5+
const DEFAULT_PROPS: InstanceType<typeof KeyValueInput>['$props'] = {
6+
label: 'Test label',
7+
modelValue: [['', '']]
8+
};
9+
10+
describe('KeyValueInput', () => {
11+
it('renders', () => {
12+
const { getByTestId } = render(KeyValueInput, { props: DEFAULT_PROPS });
13+
expect(getByTestId('uic-key-value-input')).toBeVisible();
14+
expect(getByTestId('uic-key-value-input-key-0')).toBeVisible();
15+
expect(getByTestId('uic-key-value-input-value-0')).toBeVisible();
16+
expect(getByTestId('uic-key-value-input-delete-0')).toBeVisible();
17+
expect(getByTestId('uic-key-value-add-button')).toBeVisible();
18+
});
19+
});

src/components/KeyValueInput/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { default as KeyValueInput } from './KeyValueInput.vue';

src/main.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ export * from './components/Grid';
3232
export * from './components/Icon';
3333
export * from './components/IconButton';
3434
export * from './components/ImageFileUpload';
35+
export * from './components/KeyValueInput';
3536
export * from './components/Menu';
3637
export * from './components/Modal';
3738
export * from './components/Overlay';

0 commit comments

Comments
 (0)