From a6811a922a1f4a714e995e5281424c510723da16 Mon Sep 17 00:00:00 2001 From: kolaente Date: Fri, 17 Apr 2026 14:36:31 +0200 Subject: [PATCH] refactor(frontend): support two-col layout on FormField --- .../src/components/input/FormField.test.ts | 61 +++++++++- frontend/src/components/input/FormField.vue | 106 ++++++++++++------ 2 files changed, 133 insertions(+), 34 deletions(-) diff --git a/frontend/src/components/input/FormField.test.ts b/frontend/src/components/input/FormField.test.ts index 8fe5f64ca..9e411e91c 100644 --- a/frontend/src/components/input/FormField.test.ts +++ b/frontend/src/components/input/FormField.test.ts @@ -14,7 +14,7 @@ describe('FormField', () => { const wrapper = mount(FormField, { props: { modelValue: 'initial', - 'onUpdate:modelValue': (val: string) => wrapper.setProps({modelValue: val}), + 'onUpdate:modelValue': (val: string | number) => wrapper.setProps({modelValue: val}), }, }) const input = wrapper.find('input') @@ -199,4 +199,63 @@ describe('FormField', () => { await input.setValue('test value') expect(wrapper.vm.value).toBe('test value') }) + + it('renders two-col layout with wrapping label', () => { + const wrapper = mount(FormField, { + props: {label: 'Name', layout: 'two-col'}, + slots: { + default: '', + }, + }) + const label = wrapper.find('label.two-col') + expect(label.exists()).toBe(true) + expect(label.find('span').text()).toBe('Name') + expect(label.find('input.input').exists()).toBe(true) + }) + + it('two-col layout exposes id via slot scope', () => { + const wrapper = mount({ + components: {FormField}, + template: ` + + + + `, + }) + expect(wrapper.find('input').attributes('id')).toBe('custom-id') + }) + + it('two-col layout omits the for attribute so implicit nesting labels any slotted control', () => { + const wrapper = mount(FormField, { + props: {label: 'Name', layout: 'two-col'}, + slots: { + default: '', + }, + }) + const label = wrapper.find('label.two-col') + // for would point to a different id than the slotted control generates, + // so omit it entirely and rely on the label wrapping the control. + expect(label.attributes('for')).toBeUndefined() + expect(label.find('input').exists()).toBe(true) + }) + + it('renders the error message in two-col layout', () => { + const wrapper = mount(FormField, { + props: {label: 'Name', layout: 'two-col', error: 'Required'}, + }) + const help = wrapper.find('p.help.is-danger') + expect(help.exists()).toBe(true) + expect(help.text()).toBe('Required') + }) + + it('renders the addon slot in two-col layout', () => { + const wrapper = mount(FormField, { + props: {label: 'Name', layout: 'two-col'}, + slots: { + addon: '', + }, + }) + expect(wrapper.find('.field.has-addons').exists()).toBe(true) + expect(wrapper.find('button').text()).toBe('Copy') + }) }) diff --git a/frontend/src/components/input/FormField.vue b/frontend/src/components/input/FormField.vue index 6099cdc2b..ee23a61ce 100644 --- a/frontend/src/components/input/FormField.vue +++ b/frontend/src/components/input/FormField.vue @@ -8,16 +8,18 @@ interface Props { id?: string disabled?: boolean loading?: boolean + layout?: 'stacked' | 'two-col' } -const props = defineProps() +const props = withDefaults(defineProps(), { + layout: 'stacked', +}) const emit = defineEmits<{ 'update:modelValue': [value: string | number] }>() function handleInput(event: Event) { const value = (event.target as HTMLInputElement).value - // Preserve numeric type if modelValue was a number if (typeof props.modelValue === 'number') { emit('update:modelValue', value === '' ? '' : Number(value)) } else { @@ -53,8 +55,6 @@ const inputClasses = computed(() => [ }, ]) -// Only bind value when modelValue is explicitly provided (not undefined) -// This allows the component to be used without v-model for native input behavior const inputBindings = computed(() => { const bindings: Record = {} if (props.modelValue !== undefined) { @@ -63,7 +63,6 @@ const inputBindings = computed(() => { return bindings }) -// Expose input element for direct access (needed for browser autofill workarounds) const inputRef = ref(null) defineExpose({ get value() { @@ -77,34 +76,57 @@ defineExpose({ + +