V13-FE-011: finalize search list layout slice
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
<script setup lang="ts">
|
||||
import { useUiAdapter } from '../adapter/useUiAdapter'
|
||||
import FieldShell from './FieldShell.vue'
|
||||
const props = withDefaults(defineProps<{ modelValue: number | null; label: string; currency?: string; inputId?: string; disabled?: boolean; required?: boolean; error?: string; help?: string; min?: number; max?: number }>(), { currency: 'KRW' })
|
||||
const emit = defineEmits<{ 'update:modelValue': [value: number | null]; blur: [event: FocusEvent] }>()
|
||||
const adapter = useUiAdapter()
|
||||
</script>
|
||||
<template>
|
||||
<FieldShell :label="label" :input-id="inputId" :required="required" :error="error" :help="help" v-slot="field">
|
||||
<span class="ks-money-field">
|
||||
<component
|
||||
:is="adapter.components.NumberField"
|
||||
:input-id="field.inputId"
|
||||
:model-value="modelValue"
|
||||
:disabled="disabled"
|
||||
:invalid="field.invalid"
|
||||
:min="min"
|
||||
:max="max"
|
||||
:min-fraction-digits="0"
|
||||
:max-fraction-digits="0"
|
||||
:aria-describedby="field.describedBy"
|
||||
:aria-required="field.required || undefined"
|
||||
@update:model-value="emit('update:modelValue', $event)"
|
||||
@blur="emit('blur', $event)"
|
||||
/>
|
||||
<span class="ks-money-field__currency" aria-hidden="true">{{ props.currency }}</span>
|
||||
</span>
|
||||
</FieldShell>
|
||||
</template>
|
||||
<style scoped>
|
||||
.ks-money-field { display: inline-flex; align-items: center; gap: var(--ks-space-2); }
|
||||
.ks-money-field__currency { color: var(--ks-color-text-muted); font-size: var(--ks-font-caption); font-weight: 600; }
|
||||
</style>
|
||||
@@ -0,0 +1,39 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useUiAdapter } from '../adapter/useUiAdapter'
|
||||
import FieldShell from './FieldShell.vue'
|
||||
const props = withDefaults(defineProps<{ modelValue: number | null; label: string; unit?: string; availableQuantity?: number; allowNegative?: boolean; inputId?: string; disabled?: boolean; required?: boolean; error?: string; help?: string }>(), { unit: '주', allowNegative: false })
|
||||
const emit = defineEmits<{ 'update:modelValue': [value: number | null]; blur: [event: FocusEvent] }>()
|
||||
const adapter = useUiAdapter()
|
||||
|
||||
const exceedsAvailable = computed(() =>
|
||||
props.modelValue != null && props.availableQuantity != null && props.modelValue > props.availableQuantity
|
||||
)
|
||||
const effectiveError = computed(() => props.error ?? (exceedsAvailable.value ? `가용 수량(${props.availableQuantity} ${props.unit})을 초과했습니다.` : undefined))
|
||||
const effectiveHelp = computed(() => props.availableQuantity != null ? `가용 ${props.availableQuantity} ${props.unit}` : props.help)
|
||||
</script>
|
||||
<template>
|
||||
<FieldShell :label="label" :input-id="inputId" :required="required" :error="effectiveError" :help="effectiveHelp" v-slot="field">
|
||||
<span class="ks-quantity-field">
|
||||
<component
|
||||
:is="adapter.components.NumberField"
|
||||
:input-id="field.inputId"
|
||||
:model-value="modelValue"
|
||||
:disabled="disabled"
|
||||
:invalid="field.invalid"
|
||||
:min="allowNegative ? undefined : 0"
|
||||
:min-fraction-digits="0"
|
||||
:max-fraction-digits="0"
|
||||
:aria-describedby="field.describedBy"
|
||||
:aria-required="field.required || undefined"
|
||||
@update:model-value="emit('update:modelValue', $event)"
|
||||
@blur="emit('blur', $event)"
|
||||
/>
|
||||
<span class="ks-quantity-field__unit" aria-hidden="true">{{ props.unit }}</span>
|
||||
</span>
|
||||
</FieldShell>
|
||||
</template>
|
||||
<style scoped>
|
||||
.ks-quantity-field { display: inline-flex; align-items: center; gap: var(--ks-space-2); }
|
||||
.ks-quantity-field__unit { color: var(--ks-color-text-muted); font-size: var(--ks-font-caption); font-weight: 600; }
|
||||
</style>
|
||||
@@ -6,6 +6,8 @@ export { default as KsMultiSelect } from './KsMultiSelect.vue'
|
||||
export { default as KsCheckbox } from './KsCheckbox.vue'
|
||||
export { default as KsDateField } from './KsDateField.vue'
|
||||
export { default as KsNumberField } from './KsNumberField.vue'
|
||||
export { default as KsMoneyField } from './KsMoneyField.vue'
|
||||
export { default as KsQuantityField } from './KsQuantityField.vue'
|
||||
export { default as KsDialog } from './KsDialog.vue'
|
||||
export { default as KsStatusTag } from './KsStatusTag.vue'
|
||||
export { default as KsInlineMessage } from './KsInlineMessage.vue'
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { uiAdapterKey } from '../../adapter/contracts'
|
||||
import { nativeUiAdapter } from '../../adapter/native'
|
||||
import KsMoneyField from '../KsMoneyField.vue'
|
||||
import KsQuantityField from '../KsQuantityField.vue'
|
||||
|
||||
const globalProvide = { global: { provide: { [uiAdapterKey as unknown as string]: nativeUiAdapter } } }
|
||||
|
||||
describe('KsMoneyField', () => {
|
||||
it('connects the label, shows the currency code, and emits numeric updates', async () => {
|
||||
const wrapper = mount(KsMoneyField, { props: { modelValue: 1250000, label: '단가' }, ...globalProvide })
|
||||
expect(wrapper.get('label').text()).toContain('단가')
|
||||
expect(wrapper.text()).toContain('KRW')
|
||||
await wrapper.get('input').setValue('2000000')
|
||||
expect(wrapper.emitted('update:modelValue')?.[0]).toEqual([2000000])
|
||||
})
|
||||
|
||||
it('accepts a non-KRW currency code', () => {
|
||||
const wrapper = mount(KsMoneyField, { props: { modelValue: 100, label: '금액', currency: 'USD' }, ...globalProvide })
|
||||
expect(wrapper.text()).toContain('USD')
|
||||
})
|
||||
})
|
||||
|
||||
describe('KsQuantityField', () => {
|
||||
it('shows the available quantity as help text when under the limit', () => {
|
||||
const wrapper = mount(KsQuantityField, { props: { modelValue: 5, label: '주문수량', unit: 'EA', availableQuantity: 8 }, ...globalProvide })
|
||||
expect(wrapper.text()).toContain('가용 8 EA')
|
||||
expect(wrapper.find('[role="alert"]').exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('raises an accessible error when the value exceeds the available quantity', () => {
|
||||
const wrapper = mount(KsQuantityField, { props: { modelValue: 10, label: '주문수량', unit: 'EA', availableQuantity: 8 }, ...globalProvide })
|
||||
expect(wrapper.get('[role="alert"]').text()).toContain('가용 수량(8 EA)을 초과했습니다.')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user