feat: standardize FieldShell accessibility boundary (AEG-V16-017)

Centralize label, error, help, required, and ARIA relationships across core field wrappers. Preserve WBS evidence and keep the item IN_PROGRESS pending predecessor acceptance evidence.

Evidence: frontend pnpm typecheck; pnpm test (42/42); pnpm build.
This commit is contained in:
2026-08-08 12:51:51 +09:00
parent 8c777df66b
commit 7304aafc83
16 changed files with 458 additions and 352 deletions
@@ -0,0 +1,27 @@
# AEG-V16-017 — FieldShell standardization
## Scope
- **WBS / Requirement / UI / Test:** AEG-V16-017 / REQ-V16-FEC-01 / UI-V16-FEC-01 / T-V16-FEC-01
- **Classification:** one FE component vertical slice; no API, database, policy, provider, or design-token contract change.
- **Target gate:** MVP-A. This note records implementation evidence only; it does not claim the gate is passed.
## Source / Assumption / Unknown / Decision Required
- **Source:** `docs/CURRENT/CATALOGS/WBS_MASTER.csv`, `docs/CURRENT/CATALOGS/WBS_PROGRESS_TRACKER.csv`, `contracts/ui/ui-adapter.v4.json`, and the existing components under `frontend/src/shared/ui/components/`.
- **Assumption:** the v4 provider adapters preserve fall-through ARIA attributes on their concrete input control. This is characterized by the shared-component test; provider-level evidence remains owned by AEG-V16-013 and AEG-V16-066.
- **Unknown:** the tracker has no rows for AEG-V16-009 through AEG-V16-016, while the Master marks them PLANNED and their source artifacts exist. Their acceptance evidence has not been reconstructed in this slice.
- **Decision Required:** a WBS owner must reconcile the Master and progress tracker and attach/approve the AEG-V16-016 vendor-boundary evidence before AEG-V16-017 can be marked COMPLETED.
## Acceptance mapping
| Acceptance requirement | Implementation evidence |
| --- | --- |
| label/error/help/ARIA single boundary | `FieldShell.vue` owns IDs, label, required indicator, invalid state, and message relationship. |
| no repeated field chrome in feature components | text, textarea, select, date, and number wrappers render `FieldShell`. |
| reproducible regression check | `FieldShell.spec.ts`, `pnpm typecheck`, and `pnpm test` result are recorded in the tracker after execution. |
## Non-goals
- Provider selection, capability changes, visual approval, and runtime API behavior.
- Multi-select and checkbox semantics: their v4 provider contracts do not yet expose the normalized field-control props needed for a safe conversion.
@@ -6,6 +6,7 @@ AEG-X-004,S0,Cross,DbUp 복구 rehearsal 고도화,COMPLETED,2026-08-06,"docs/CU
AEG-X-005,S0,Cross,Security auth 고도화,COMPLETED,2026-08-04,"docs/decisions/ADR-SEC-001.md + tests/KArtSell.Integration.Tests/SecurityAuthenticationTests.cs (6 tests)",Security/BE,"✅ ADR-SEC-001 produced (OIDC/JWT/DevelopmentHeader tiers), SecurityAuthenticationTests.cs (6 tests): endpoint authorization, DevelopmentHeader mode check, secret logging prevention, secret hardcoding check, AI prompt PII, auth config validation. Acceptance_Evidence verified: '비개발 무인증 접근 0, secret/log/prompt 노출 0'"
AEG-X-006,S0,Cross,Outbox publisher 고도화,COMPLETED,2026-08-04,"docs/CURRENT/ARTIFACTS/AEG-X-006_ACCEPTANCE_EVIDENCE.md + src/KArtSell.BuildingBlocks/Reliability/DapperOutboxWriter.cs + OutboxPollerJob.cs",BE/SRE,"✅ Outbox→Inbox async pipeline verified: DapperOutboxWriter (transactional), OutboxPollerJob (idempotent), DapperInboxStore (deduplication), 5 consumer implementations. Acceptance_Evidence: All criteria met. 177/177 tests PASS."
AEG-X-007,S0,Cross,Serilog/OTel correlation 고도화,COMPLETED,2026-08-06,"tests/KArtSell.ArchitectureTests/PiiRedactionTests.cs (6 tests) + commit e7913db",SRE/Security,"✅ PII redaction policy VERIFIED: SSN/Email/CreditCard/ApiKey redaction (6 tests). Commit e7913db adds pattern-based sanitization validation. All tests PASS (249/253)."
AEG-V16-017,S6,Cross,FieldShell 표준,IN_PROGRESS,TBD,"docs/CURRENT/AEG-V16-017_FIELDSHELL_SLICE_NOTE.md; frontend/src/shared/ui/components/FieldShell.vue; frontend/src/shared/ui/components/tests/FieldShell.spec.ts","FE Lead","2026-08-08: FieldShell now owns label/error/help/ARIA relationships for KsTextField, KsTextArea, KsSelect, KsDateField, and KsNumberField. Actual evidence: frontend pnpm typecheck PASS; pnpm test PASS (19 files, 42 tests); pnpm build PASS. Build emitted unrelated tracked .js drift, excluded from this Slice. COMPLETED is blocked pending WBS Master/tracker reconciliation and AEG-V16-016 vendor-boundary acceptance evidence."
AEG-X-008,S0,Cross,OpenAPI artifact 고도화,COMPLETED,2026-08-04,.gitea/workflows/openapi-gate.yml + docs/api/openapi.json,BE/FE Architect,"✅ OpenAPI diff gate implemented: CI/CD automation detects breaking changes (3 checks: parameter removal, status code removal, field removal), blocks merge without approval, auto-comments on PR"
AEG-VS-00-01,S0,VS-00,정책·범위·실패상태 계약 확정,COMPLETED,2026-08-06,"docs/CURRENT/SLICE_SPECS/VS-00-SLICE_SPEC.md + commit e7913db",PM/Architect,"✅ SLICE_SPEC produced: VS-00-SLICE_SPEC.md (state transitions, RBAC, governance gates, DQ rules, compliance). Commit e7913db. 249/253 tests PASS."
AEG-VS-00-02,S0,VS-00,데이터 시점·스키마·정합성 계약,COMPLETED,2026-08-06,"contracts/data/platform-data-contract.v1.json + commit e7913db",Data Architect/DBA,"✅ DATA_CONTRACT v1.0 produced: PIT envelope (published_at/correlation_id/revision), 5 table schemas, DQ rules/lineage, GDPR/PCI-DSS compliance. JSON schema + validation. 249/253 tests PASS."
1 WBS_ID Sprint Slice_ID Task Status Completion_Date Evidence_Link Owner Notes
6 AEG-X-005 S0 Cross Security auth 고도화 COMPLETED 2026-08-04 docs/decisions/ADR-SEC-001.md + tests/KArtSell.Integration.Tests/SecurityAuthenticationTests.cs (6 tests) Security/BE ✅ ADR-SEC-001 produced (OIDC/JWT/DevelopmentHeader tiers), SecurityAuthenticationTests.cs (6 tests): endpoint authorization, DevelopmentHeader mode check, secret logging prevention, secret hardcoding check, AI prompt PII, auth config validation. Acceptance_Evidence verified: '비개발 무인증 접근 0, secret/log/prompt 노출 0'
7 AEG-X-006 S0 Cross Outbox publisher 고도화 COMPLETED 2026-08-04 docs/CURRENT/ARTIFACTS/AEG-X-006_ACCEPTANCE_EVIDENCE.md + src/KArtSell.BuildingBlocks/Reliability/DapperOutboxWriter.cs + OutboxPollerJob.cs BE/SRE ✅ Outbox→Inbox async pipeline verified: DapperOutboxWriter (transactional), OutboxPollerJob (idempotent), DapperInboxStore (deduplication), 5 consumer implementations. Acceptance_Evidence: All criteria met. 177/177 tests PASS.
8 AEG-X-007 S0 Cross Serilog/OTel correlation 고도화 COMPLETED 2026-08-06 tests/KArtSell.ArchitectureTests/PiiRedactionTests.cs (6 tests) + commit e7913db SRE/Security ✅ PII redaction policy VERIFIED: SSN/Email/CreditCard/ApiKey redaction (6 tests). Commit e7913db adds pattern-based sanitization validation. All tests PASS (249/253).
9 AEG-V16-017 S6 Cross FieldShell 표준 IN_PROGRESS TBD docs/CURRENT/AEG-V16-017_FIELDSHELL_SLICE_NOTE.md; frontend/src/shared/ui/components/FieldShell.vue; frontend/src/shared/ui/components/tests/FieldShell.spec.ts FE Lead 2026-08-08: FieldShell now owns label/error/help/ARIA relationships for KsTextField, KsTextArea, KsSelect, KsDateField, and KsNumberField. Actual evidence: frontend pnpm typecheck PASS; pnpm test PASS (19 files, 42 tests); pnpm build PASS. Build emitted unrelated tracked .js drift, excluded from this Slice. COMPLETED is blocked pending WBS Master/tracker reconciliation and AEG-V16-016 vendor-boundary acceptance evidence.
10 AEG-X-008 S0 Cross OpenAPI artifact 고도화 COMPLETED 2026-08-04 .gitea/workflows/openapi-gate.yml + docs/api/openapi.json BE/FE Architect ✅ OpenAPI diff gate implemented: CI/CD automation detects breaking changes (3 checks: parameter removal, status code removal, field removal), blocks merge without approval, auto-comments on PR
11 AEG-VS-00-01 S0 VS-00 정책·범위·실패상태 계약 확정 COMPLETED 2026-08-06 docs/CURRENT/SLICE_SPECS/VS-00-SLICE_SPEC.md + commit e7913db PM/Architect ✅ SLICE_SPEC produced: VS-00-SLICE_SPEC.md (state transitions, RBAC, governance gates, DQ rules, compliance). Commit e7913db. 249/253 tests PASS.
12 AEG-VS-00-02 S0 VS-00 데이터 시점·스키마·정합성 계약 COMPLETED 2026-08-06 contracts/data/platform-data-contract.v1.json + commit e7913db Data Architect/DBA ✅ DATA_CONTRACT v1.0 produced: PIT envelope (published_at/correlation_id/revision), 5 table schemas, DQ rules/lineage, GDPR/PCI-DSS compliance. JSON schema + validation. 249/253 tests PASS.
@@ -8,7 +8,7 @@ const messageId = computed(() => props.error || props.help ? `${resolvedId.value
<template>
<div class="ks-field-shell" :data-invalid="Boolean(error) || undefined">
<label :for="resolvedId">{{ label }} <span v-if="required" aria-hidden="true">*</span></label>
<slot :input-id="resolvedId" :described-by="messageId" :invalid="Boolean(error)" />
<slot :input-id="resolvedId" :described-by="messageId" :invalid="Boolean(error)" :required="Boolean(required)" />
<small v-if="error || help" :id="messageId" :class="{ 'ks-danger-text': error }" :role="error ? 'alert' : undefined">{{ error ?? help }}</small>
</div>
</template>
@@ -30,6 +30,7 @@ var __VLS_0 = {
inputId: (__VLS_ctx.resolvedId),
describedBy: (__VLS_ctx.messageId),
invalid: (Boolean(__VLS_ctx.error)),
required: (Boolean(__VLS_ctx.required)),
};
if (__VLS_ctx.error || __VLS_ctx.help) {
__VLS_asFunctionalElement1(__VLS_intrinsics.small, __VLS_intrinsics.small)({
@@ -43,7 +44,7 @@ if (__VLS_ctx.error || __VLS_ctx.help) {
// @ts-ignore
var __VLS_1 = __VLS_0;
// @ts-ignore
[error, error, error, error, error, error, resolvedId, resolvedId, label, required, messageId, messageId, help, help,];
[error, error, error, error, error, error, resolvedId, resolvedId, label, required, required, messageId, messageId, help, help,];
const __VLS_base = (await import('vue')).defineComponent({
__typeProps: {},
});
@@ -1,11 +1,8 @@
<script setup lang="ts">
import { computed, useId } from 'vue'
import { useUiAdapter } from '../adapter/useUiAdapter'
import FieldShell from './FieldShell.vue'
const props = defineProps<{ modelValue: string | Date | null; label: string; inputId?: string; disabled?: boolean; required?: boolean; error?: string; help?: string; min?: Date; max?: Date }>()
const emit = defineEmits<{ 'update:modelValue': [value: string | Date | null]; blur: [event: FocusEvent] }>()
const adapter = useUiAdapter()
const generatedId = useId()
const resolvedId = computed(() => props.inputId ?? `ks-date-${generatedId}`)
</script>
<template><div class="ks-field"><label :for="resolvedId">{{ label }} <span v-if="required" aria-hidden="true">*</span></label><component :is="adapter.components.DateField" :input-id="resolvedId" :model-value="modelValue" :disabled="disabled" :invalid="Boolean(error)" :min="min" :max="max" :aria-describedby="error || help ? `${resolvedId}-message` : undefined" @update:model-value="emit('update:modelValue', $event)" @blur="emit('blur', $event)" /><small v-if="error || help" :id="`${resolvedId}-message`" :class="{ 'ks-danger-text': error }">{{ error ?? help }}</small></div></template>
<style scoped>.ks-field{display:grid;gap:var(--ks-space-1)}label{font-weight:650}small{color:var(--ks-color-neutral-600)}</style>
<template><FieldShell :label="label" :input-id="inputId" :required="required" :error="error" :help="help" v-slot="field"><component :is="adapter.components.DateField" :input-id="field.inputId" :model-value="modelValue" :disabled="disabled" :invalid="field.invalid" :min="min" :max="max" :aria-describedby="field.describedBy" :aria-required="field.required || undefined" @update:model-value="emit('update:modelValue', $event)" @blur="emit('blur', $event)" /></FieldShell></template>
@@ -1,10 +1,8 @@
import { computed, useId } from 'vue';
import { useUiAdapter } from '../adapter/useUiAdapter';
import FieldShell from './FieldShell.vue';
const props = defineProps();
const emit = defineEmits();
const adapter = useUiAdapter();
const generatedId = useId();
const resolvedId = computed(() => props.inputId ?? `ks-date-${generatedId}`);
const __VLS_ctx = {
...{},
...{},
@@ -15,72 +13,78 @@ const __VLS_ctx = {
let __VLS_components;
let __VLS_intrinsics;
let __VLS_directives;
__VLS_asFunctionalElement1(__VLS_intrinsics.div, __VLS_intrinsics.div)({
...{ class: "ks-field" },
});
/** @type {__VLS_StyleScopedClasses['ks-field']} */ ;
__VLS_asFunctionalElement1(__VLS_intrinsics.label, __VLS_intrinsics.label)({
for: (__VLS_ctx.resolvedId),
});
(__VLS_ctx.label);
if (__VLS_ctx.required) {
__VLS_asFunctionalElement1(__VLS_intrinsics.span, __VLS_intrinsics.span)({
'aria-hidden': "true",
});
}
const __VLS_0 = (__VLS_ctx.adapter.components.DateField);
const __VLS_0 = FieldShell || FieldShell;
// @ts-ignore
const __VLS_1 = __VLS_asFunctionalComponent1(__VLS_0, new __VLS_0({
...{ 'onUpdate:modelValue': {} },
...{ 'onBlur': {} },
inputId: (__VLS_ctx.resolvedId),
modelValue: (__VLS_ctx.modelValue),
disabled: (__VLS_ctx.disabled),
invalid: (Boolean(__VLS_ctx.error)),
min: (__VLS_ctx.min),
max: (__VLS_ctx.max),
'aria-describedby': (__VLS_ctx.error || __VLS_ctx.help ? `${__VLS_ctx.resolvedId}-message` : undefined),
label: (__VLS_ctx.label),
inputId: (__VLS_ctx.inputId),
required: (__VLS_ctx.required),
error: (__VLS_ctx.error),
help: (__VLS_ctx.help),
}));
const __VLS_2 = __VLS_1({
...{ 'onUpdate:modelValue': {} },
...{ 'onBlur': {} },
inputId: (__VLS_ctx.resolvedId),
modelValue: (__VLS_ctx.modelValue),
disabled: (__VLS_ctx.disabled),
invalid: (Boolean(__VLS_ctx.error)),
min: (__VLS_ctx.min),
max: (__VLS_ctx.max),
'aria-describedby': (__VLS_ctx.error || __VLS_ctx.help ? `${__VLS_ctx.resolvedId}-message` : undefined),
label: (__VLS_ctx.label),
inputId: (__VLS_ctx.inputId),
required: (__VLS_ctx.required),
error: (__VLS_ctx.error),
help: (__VLS_ctx.help),
}, ...__VLS_functionalComponentArgsRest(__VLS_1));
let __VLS_5;
const __VLS_6 = {
/** @type {typeof __VLS_5.'update:modelValue'} */
'onUpdate:modelValue': (...[$event]) => {
return (__VLS_ctx.emit('update:modelValue', $event));
// @ts-ignore
[resolvedId, resolvedId, resolvedId, label, required, adapter, modelValue, disabled, error, error, min, max, help, emit,];
},
};
const __VLS_7 = {
/** @type {typeof __VLS_5.blur} */
onBlur: (...[$event]) => {
return (__VLS_ctx.emit('blur', $event));
// @ts-ignore
[emit,];
},
};
var __VLS_3;
var __VLS_4;
if (__VLS_ctx.error || __VLS_ctx.help) {
__VLS_asFunctionalElement1(__VLS_intrinsics.small, __VLS_intrinsics.small)({
id: (`${__VLS_ctx.resolvedId}-message`),
...{ class: ({ 'ks-danger-text': __VLS_ctx.error }) },
});
/** @type {__VLS_StyleScopedClasses['ks-danger-text']} */ ;
(__VLS_ctx.error ?? __VLS_ctx.help);
var __VLS_5;
{
const { default: __VLS_6 } = __VLS_3.slots;
const [field] = __VLS_vSlot(__VLS_6);
const __VLS_7 = (__VLS_ctx.adapter.components.DateField);
// @ts-ignore
const __VLS_8 = __VLS_asFunctionalComponent1(__VLS_7, new __VLS_7({
...{ 'onUpdate:modelValue': {} },
...{ 'onBlur': {} },
inputId: (field.inputId),
modelValue: (__VLS_ctx.modelValue),
disabled: (__VLS_ctx.disabled),
invalid: (field.invalid),
min: (__VLS_ctx.min),
max: (__VLS_ctx.max),
'aria-describedby': (field.describedBy),
'aria-required': (field.required || undefined),
}));
const __VLS_9 = __VLS_8({
...{ 'onUpdate:modelValue': {} },
...{ 'onBlur': {} },
inputId: (field.inputId),
modelValue: (__VLS_ctx.modelValue),
disabled: (__VLS_ctx.disabled),
invalid: (field.invalid),
min: (__VLS_ctx.min),
max: (__VLS_ctx.max),
'aria-describedby': (field.describedBy),
'aria-required': (field.required || undefined),
}, ...__VLS_functionalComponentArgsRest(__VLS_8));
let __VLS_12;
const __VLS_13 = {
/** @type {typeof __VLS_12.'update:modelValue'} */
'onUpdate:modelValue': (...[$event]) => {
return (__VLS_ctx.emit('update:modelValue', $event));
// @ts-ignore
[label, inputId, required, error, help, adapter, modelValue, disabled, min, max, emit,];
},
};
const __VLS_14 = {
/** @type {typeof __VLS_12.blur} */
onBlur: (...[$event]) => {
return (__VLS_ctx.emit('blur', $event));
// @ts-ignore
[emit,];
},
};
var __VLS_10;
var __VLS_11;
// @ts-ignore
[];
__VLS_3.slots['' /* empty slot name completion */];
}
var __VLS_3;
// @ts-ignore
[resolvedId, error, error, error, help, help,];
[];
const __VLS_export = (await import('vue')).defineComponent({
__typeEmits: {},
__typeProps: {},
@@ -1,11 +1,8 @@
<script setup lang="ts">
import { computed, useId } from 'vue'
import { useUiAdapter } from '../adapter/useUiAdapter'
import FieldShell from './FieldShell.vue'
const props = defineProps<{ modelValue: number | null; label: string; inputId?: string; disabled?: boolean; required?: boolean; error?: string; help?: string; min?: number; max?: number; minFractionDigits?: number; maxFractionDigits?: number }>()
const emit = defineEmits<{ 'update:modelValue': [value: number | null]; blur: [event: FocusEvent] }>()
const adapter = useUiAdapter()
const generatedId = useId()
const resolvedId = computed(() => props.inputId ?? `ks-number-${generatedId}`)
</script>
<template><div class="ks-field"><label :for="resolvedId">{{ label }} <span v-if="required" aria-hidden="true">*</span></label><component :is="adapter.components.NumberField" :input-id="resolvedId" :model-value="modelValue" :disabled="disabled" :invalid="Boolean(error)" :min="min" :max="max" :min-fraction-digits="minFractionDigits" :max-fraction-digits="maxFractionDigits" :aria-describedby="error || help ? `${resolvedId}-message` : undefined" @update:model-value="emit('update:modelValue', $event)" @blur="emit('blur', $event)" /><small v-if="error || help" :id="`${resolvedId}-message`" :class="{ 'ks-danger-text': error }">{{ error ?? help }}</small></div></template>
<style scoped>.ks-field{display:grid;gap:var(--ks-space-1)}label{font-weight:650}small{color:var(--ks-color-neutral-600)}</style>
<template><FieldShell :label="label" :input-id="inputId" :required="required" :error="error" :help="help" v-slot="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="minFractionDigits" :max-fraction-digits="maxFractionDigits" :aria-describedby="field.describedBy" :aria-required="field.required || undefined" @update:model-value="emit('update:modelValue', $event)" @blur="emit('blur', $event)" /></FieldShell></template>
@@ -1,10 +1,8 @@
import { computed, useId } from 'vue';
import { useUiAdapter } from '../adapter/useUiAdapter';
import FieldShell from './FieldShell.vue';
const props = defineProps();
const emit = defineEmits();
const adapter = useUiAdapter();
const generatedId = useId();
const resolvedId = computed(() => props.inputId ?? `ks-number-${generatedId}`);
const __VLS_ctx = {
...{},
...{},
@@ -15,76 +13,82 @@ const __VLS_ctx = {
let __VLS_components;
let __VLS_intrinsics;
let __VLS_directives;
__VLS_asFunctionalElement1(__VLS_intrinsics.div, __VLS_intrinsics.div)({
...{ class: "ks-field" },
});
/** @type {__VLS_StyleScopedClasses['ks-field']} */ ;
__VLS_asFunctionalElement1(__VLS_intrinsics.label, __VLS_intrinsics.label)({
for: (__VLS_ctx.resolvedId),
});
(__VLS_ctx.label);
if (__VLS_ctx.required) {
__VLS_asFunctionalElement1(__VLS_intrinsics.span, __VLS_intrinsics.span)({
'aria-hidden': "true",
});
}
const __VLS_0 = (__VLS_ctx.adapter.components.NumberField);
const __VLS_0 = FieldShell || FieldShell;
// @ts-ignore
const __VLS_1 = __VLS_asFunctionalComponent1(__VLS_0, new __VLS_0({
...{ 'onUpdate:modelValue': {} },
...{ 'onBlur': {} },
inputId: (__VLS_ctx.resolvedId),
modelValue: (__VLS_ctx.modelValue),
disabled: (__VLS_ctx.disabled),
invalid: (Boolean(__VLS_ctx.error)),
min: (__VLS_ctx.min),
max: (__VLS_ctx.max),
minFractionDigits: (__VLS_ctx.minFractionDigits),
maxFractionDigits: (__VLS_ctx.maxFractionDigits),
'aria-describedby': (__VLS_ctx.error || __VLS_ctx.help ? `${__VLS_ctx.resolvedId}-message` : undefined),
label: (__VLS_ctx.label),
inputId: (__VLS_ctx.inputId),
required: (__VLS_ctx.required),
error: (__VLS_ctx.error),
help: (__VLS_ctx.help),
}));
const __VLS_2 = __VLS_1({
...{ 'onUpdate:modelValue': {} },
...{ 'onBlur': {} },
inputId: (__VLS_ctx.resolvedId),
modelValue: (__VLS_ctx.modelValue),
disabled: (__VLS_ctx.disabled),
invalid: (Boolean(__VLS_ctx.error)),
min: (__VLS_ctx.min),
max: (__VLS_ctx.max),
minFractionDigits: (__VLS_ctx.minFractionDigits),
maxFractionDigits: (__VLS_ctx.maxFractionDigits),
'aria-describedby': (__VLS_ctx.error || __VLS_ctx.help ? `${__VLS_ctx.resolvedId}-message` : undefined),
label: (__VLS_ctx.label),
inputId: (__VLS_ctx.inputId),
required: (__VLS_ctx.required),
error: (__VLS_ctx.error),
help: (__VLS_ctx.help),
}, ...__VLS_functionalComponentArgsRest(__VLS_1));
let __VLS_5;
const __VLS_6 = {
/** @type {typeof __VLS_5.'update:modelValue'} */
'onUpdate:modelValue': (...[$event]) => {
return (__VLS_ctx.emit('update:modelValue', $event));
// @ts-ignore
[resolvedId, resolvedId, resolvedId, label, required, adapter, modelValue, disabled, error, error, min, max, minFractionDigits, maxFractionDigits, help, emit,];
},
};
const __VLS_7 = {
/** @type {typeof __VLS_5.blur} */
onBlur: (...[$event]) => {
return (__VLS_ctx.emit('blur', $event));
// @ts-ignore
[emit,];
},
};
var __VLS_3;
var __VLS_4;
if (__VLS_ctx.error || __VLS_ctx.help) {
__VLS_asFunctionalElement1(__VLS_intrinsics.small, __VLS_intrinsics.small)({
id: (`${__VLS_ctx.resolvedId}-message`),
...{ class: ({ 'ks-danger-text': __VLS_ctx.error }) },
});
/** @type {__VLS_StyleScopedClasses['ks-danger-text']} */ ;
(__VLS_ctx.error ?? __VLS_ctx.help);
var __VLS_5;
{
const { default: __VLS_6 } = __VLS_3.slots;
const [field] = __VLS_vSlot(__VLS_6);
const __VLS_7 = (__VLS_ctx.adapter.components.NumberField);
// @ts-ignore
const __VLS_8 = __VLS_asFunctionalComponent1(__VLS_7, new __VLS_7({
...{ 'onUpdate:modelValue': {} },
...{ 'onBlur': {} },
inputId: (field.inputId),
modelValue: (__VLS_ctx.modelValue),
disabled: (__VLS_ctx.disabled),
invalid: (field.invalid),
min: (__VLS_ctx.min),
max: (__VLS_ctx.max),
minFractionDigits: (__VLS_ctx.minFractionDigits),
maxFractionDigits: (__VLS_ctx.maxFractionDigits),
'aria-describedby': (field.describedBy),
'aria-required': (field.required || undefined),
}));
const __VLS_9 = __VLS_8({
...{ 'onUpdate:modelValue': {} },
...{ 'onBlur': {} },
inputId: (field.inputId),
modelValue: (__VLS_ctx.modelValue),
disabled: (__VLS_ctx.disabled),
invalid: (field.invalid),
min: (__VLS_ctx.min),
max: (__VLS_ctx.max),
minFractionDigits: (__VLS_ctx.minFractionDigits),
maxFractionDigits: (__VLS_ctx.maxFractionDigits),
'aria-describedby': (field.describedBy),
'aria-required': (field.required || undefined),
}, ...__VLS_functionalComponentArgsRest(__VLS_8));
let __VLS_12;
const __VLS_13 = {
/** @type {typeof __VLS_12.'update:modelValue'} */
'onUpdate:modelValue': (...[$event]) => {
return (__VLS_ctx.emit('update:modelValue', $event));
// @ts-ignore
[label, inputId, required, error, help, adapter, modelValue, disabled, min, max, minFractionDigits, maxFractionDigits, emit,];
},
};
const __VLS_14 = {
/** @type {typeof __VLS_12.blur} */
onBlur: (...[$event]) => {
return (__VLS_ctx.emit('blur', $event));
// @ts-ignore
[emit,];
},
};
var __VLS_10;
var __VLS_11;
// @ts-ignore
[];
__VLS_3.slots['' /* empty slot name completion */];
}
var __VLS_3;
// @ts-ignore
[resolvedId, error, error, error, help, help,];
[];
const __VLS_export = (await import('vue')).defineComponent({
__typeEmits: {},
__typeProps: {},
@@ -1,18 +1,13 @@
<script setup lang="ts">
import { computed, useId } from 'vue'
import type { UiSelectOption } from '../adapter/contracts'
import { useUiAdapter } from '../adapter/useUiAdapter'
import FieldShell from './FieldShell.vue'
const props = defineProps<{ modelValue: unknown; label: string; options: UiSelectOption[]; inputId?: string; disabled?: boolean; required?: boolean; error?: string; help?: string; placeholder?: string }>()
const emit = defineEmits<{ 'update:modelValue': [value: unknown]; blur: [event: FocusEvent] }>()
const adapter = useUiAdapter()
const generatedId = useId()
const resolvedId = computed(() => props.inputId ?? `ks-select-${generatedId}`)
</script>
<template>
<div class="ks-field">
<label :for="resolvedId">{{ label }} <span v-if="required" aria-hidden="true">*</span></label>
<component :is="adapter.components.Select" :input-id="resolvedId" :model-value="modelValue" :options="options" :disabled="disabled" :invalid="Boolean(error)" :placeholder="placeholder" @update:model-value="emit('update:modelValue', $event)" @blur="emit('blur', $event)" />
<small v-if="error || help" :class="{ 'ks-danger-text': error }">{{ error ?? help }}</small>
</div>
<FieldShell :label="label" :input-id="inputId" :required="required" :error="error" :help="help" v-slot="field">
<component :is="adapter.components.Select" :input-id="field.inputId" :model-value="modelValue" :options="options" :disabled="disabled" :invalid="field.invalid" :placeholder="placeholder" :aria-describedby="field.describedBy" :aria-required="field.required || undefined" @update:model-value="emit('update:modelValue', $event)" @blur="emit('blur', $event)" />
</FieldShell>
</template>
<style scoped>.ks-field { display: grid; gap: var(--ks-space-1); } label { font-weight: 650; } small { color: var(--ks-color-neutral-600); }</style>
@@ -1,10 +1,8 @@
import { computed, useId } from 'vue';
import { useUiAdapter } from '../adapter/useUiAdapter';
import FieldShell from './FieldShell.vue';
const props = defineProps();
const emit = defineEmits();
const adapter = useUiAdapter();
const generatedId = useId();
const resolvedId = computed(() => props.inputId ?? `ks-select-${generatedId}`);
const __VLS_ctx = {
...{},
...{},
@@ -15,69 +13,78 @@ const __VLS_ctx = {
let __VLS_components;
let __VLS_intrinsics;
let __VLS_directives;
__VLS_asFunctionalElement1(__VLS_intrinsics.div, __VLS_intrinsics.div)({
...{ class: "ks-field" },
});
/** @type {__VLS_StyleScopedClasses['ks-field']} */ ;
__VLS_asFunctionalElement1(__VLS_intrinsics.label, __VLS_intrinsics.label)({
for: (__VLS_ctx.resolvedId),
});
(__VLS_ctx.label);
if (__VLS_ctx.required) {
__VLS_asFunctionalElement1(__VLS_intrinsics.span, __VLS_intrinsics.span)({
'aria-hidden': "true",
});
}
const __VLS_0 = (__VLS_ctx.adapter.components.Select);
const __VLS_0 = FieldShell || FieldShell;
// @ts-ignore
const __VLS_1 = __VLS_asFunctionalComponent1(__VLS_0, new __VLS_0({
...{ 'onUpdate:modelValue': {} },
...{ 'onBlur': {} },
inputId: (__VLS_ctx.resolvedId),
modelValue: (__VLS_ctx.modelValue),
options: (__VLS_ctx.options),
disabled: (__VLS_ctx.disabled),
invalid: (Boolean(__VLS_ctx.error)),
placeholder: (__VLS_ctx.placeholder),
label: (__VLS_ctx.label),
inputId: (__VLS_ctx.inputId),
required: (__VLS_ctx.required),
error: (__VLS_ctx.error),
help: (__VLS_ctx.help),
}));
const __VLS_2 = __VLS_1({
...{ 'onUpdate:modelValue': {} },
...{ 'onBlur': {} },
inputId: (__VLS_ctx.resolvedId),
modelValue: (__VLS_ctx.modelValue),
options: (__VLS_ctx.options),
disabled: (__VLS_ctx.disabled),
invalid: (Boolean(__VLS_ctx.error)),
placeholder: (__VLS_ctx.placeholder),
label: (__VLS_ctx.label),
inputId: (__VLS_ctx.inputId),
required: (__VLS_ctx.required),
error: (__VLS_ctx.error),
help: (__VLS_ctx.help),
}, ...__VLS_functionalComponentArgsRest(__VLS_1));
let __VLS_5;
const __VLS_6 = {
/** @type {typeof __VLS_5.'update:modelValue'} */
'onUpdate:modelValue': (...[$event]) => {
return (__VLS_ctx.emit('update:modelValue', $event));
// @ts-ignore
[resolvedId, resolvedId, label, required, adapter, modelValue, options, disabled, error, placeholder, emit,];
},
};
const __VLS_7 = {
/** @type {typeof __VLS_5.blur} */
onBlur: (...[$event]) => {
return (__VLS_ctx.emit('blur', $event));
// @ts-ignore
[emit,];
},
};
var __VLS_3;
var __VLS_4;
if (__VLS_ctx.error || __VLS_ctx.help) {
__VLS_asFunctionalElement1(__VLS_intrinsics.small, __VLS_intrinsics.small)({
...{ class: ({ 'ks-danger-text': __VLS_ctx.error }) },
});
/** @type {__VLS_StyleScopedClasses['ks-danger-text']} */ ;
(__VLS_ctx.error ?? __VLS_ctx.help);
var __VLS_5;
{
const { default: __VLS_6 } = __VLS_3.slots;
const [field] = __VLS_vSlot(__VLS_6);
const __VLS_7 = (__VLS_ctx.adapter.components.Select);
// @ts-ignore
const __VLS_8 = __VLS_asFunctionalComponent1(__VLS_7, new __VLS_7({
...{ 'onUpdate:modelValue': {} },
...{ 'onBlur': {} },
inputId: (field.inputId),
modelValue: (__VLS_ctx.modelValue),
options: (__VLS_ctx.options),
disabled: (__VLS_ctx.disabled),
invalid: (field.invalid),
placeholder: (__VLS_ctx.placeholder),
'aria-describedby': (field.describedBy),
'aria-required': (field.required || undefined),
}));
const __VLS_9 = __VLS_8({
...{ 'onUpdate:modelValue': {} },
...{ 'onBlur': {} },
inputId: (field.inputId),
modelValue: (__VLS_ctx.modelValue),
options: (__VLS_ctx.options),
disabled: (__VLS_ctx.disabled),
invalid: (field.invalid),
placeholder: (__VLS_ctx.placeholder),
'aria-describedby': (field.describedBy),
'aria-required': (field.required || undefined),
}, ...__VLS_functionalComponentArgsRest(__VLS_8));
let __VLS_12;
const __VLS_13 = {
/** @type {typeof __VLS_12.'update:modelValue'} */
'onUpdate:modelValue': (...[$event]) => {
return (__VLS_ctx.emit('update:modelValue', $event));
// @ts-ignore
[label, inputId, required, error, help, adapter, modelValue, options, disabled, placeholder, emit,];
},
};
const __VLS_14 = {
/** @type {typeof __VLS_12.blur} */
onBlur: (...[$event]) => {
return (__VLS_ctx.emit('blur', $event));
// @ts-ignore
[emit,];
},
};
var __VLS_10;
var __VLS_11;
// @ts-ignore
[];
__VLS_3.slots['' /* empty slot name completion */];
}
var __VLS_3;
// @ts-ignore
[error, error, error, help, help,];
[];
const __VLS_export = (await import('vue')).defineComponent({
__typeEmits: {},
__typeProps: {},
@@ -1,17 +1,12 @@
<script setup lang="ts">
import { computed, useId } from 'vue'
import { useUiAdapter } from '../adapter/useUiAdapter'
import FieldShell from './FieldShell.vue'
const props = defineProps<{ modelValue: string; label: string; inputId?: string; disabled?: boolean; required?: boolean; error?: string; help?: string; rows?: number; placeholder?: string }>()
const emit = defineEmits<{ 'update:modelValue': [value: string]; blur: [event: FocusEvent] }>()
const adapter = useUiAdapter()
const generatedId = useId()
const resolvedId = computed(() => props.inputId ?? `ks-area-${generatedId}`)
</script>
<template>
<div class="ks-field">
<label :for="resolvedId">{{ label }} <span v-if="required" aria-hidden="true">*</span></label>
<component :is="adapter.components.TextArea" :input-id="resolvedId" :model-value="modelValue" :disabled="disabled" :invalid="Boolean(error)" :rows="rows" :placeholder="placeholder" @update:model-value="emit('update:modelValue', $event)" @blur="emit('blur', $event)" />
<small v-if="error || help" :class="{ 'ks-danger-text': error }">{{ error ?? help }}</small>
</div>
<FieldShell :label="label" :input-id="inputId" :required="required" :error="error" :help="help" v-slot="field">
<component :is="adapter.components.TextArea" :input-id="field.inputId" :model-value="modelValue" :disabled="disabled" :invalid="field.invalid" :rows="rows" :placeholder="placeholder" :aria-describedby="field.describedBy" :aria-required="field.required || undefined" @update:model-value="emit('update:modelValue', $event)" @blur="emit('blur', $event)" />
</FieldShell>
</template>
<style scoped>.ks-field { display: grid; gap: var(--ks-space-1); } label { font-weight: 650; } small { color: var(--ks-color-neutral-600); }</style>
@@ -1,10 +1,8 @@
import { computed, useId } from 'vue';
import { useUiAdapter } from '../adapter/useUiAdapter';
import FieldShell from './FieldShell.vue';
const props = defineProps();
const emit = defineEmits();
const adapter = useUiAdapter();
const generatedId = useId();
const resolvedId = computed(() => props.inputId ?? `ks-area-${generatedId}`);
const __VLS_ctx = {
...{},
...{},
@@ -15,69 +13,78 @@ const __VLS_ctx = {
let __VLS_components;
let __VLS_intrinsics;
let __VLS_directives;
__VLS_asFunctionalElement1(__VLS_intrinsics.div, __VLS_intrinsics.div)({
...{ class: "ks-field" },
});
/** @type {__VLS_StyleScopedClasses['ks-field']} */ ;
__VLS_asFunctionalElement1(__VLS_intrinsics.label, __VLS_intrinsics.label)({
for: (__VLS_ctx.resolvedId),
});
(__VLS_ctx.label);
if (__VLS_ctx.required) {
__VLS_asFunctionalElement1(__VLS_intrinsics.span, __VLS_intrinsics.span)({
'aria-hidden': "true",
});
}
const __VLS_0 = (__VLS_ctx.adapter.components.TextArea);
const __VLS_0 = FieldShell || FieldShell;
// @ts-ignore
const __VLS_1 = __VLS_asFunctionalComponent1(__VLS_0, new __VLS_0({
...{ 'onUpdate:modelValue': {} },
...{ 'onBlur': {} },
inputId: (__VLS_ctx.resolvedId),
modelValue: (__VLS_ctx.modelValue),
disabled: (__VLS_ctx.disabled),
invalid: (Boolean(__VLS_ctx.error)),
rows: (__VLS_ctx.rows),
placeholder: (__VLS_ctx.placeholder),
label: (__VLS_ctx.label),
inputId: (__VLS_ctx.inputId),
required: (__VLS_ctx.required),
error: (__VLS_ctx.error),
help: (__VLS_ctx.help),
}));
const __VLS_2 = __VLS_1({
...{ 'onUpdate:modelValue': {} },
...{ 'onBlur': {} },
inputId: (__VLS_ctx.resolvedId),
modelValue: (__VLS_ctx.modelValue),
disabled: (__VLS_ctx.disabled),
invalid: (Boolean(__VLS_ctx.error)),
rows: (__VLS_ctx.rows),
placeholder: (__VLS_ctx.placeholder),
label: (__VLS_ctx.label),
inputId: (__VLS_ctx.inputId),
required: (__VLS_ctx.required),
error: (__VLS_ctx.error),
help: (__VLS_ctx.help),
}, ...__VLS_functionalComponentArgsRest(__VLS_1));
let __VLS_5;
const __VLS_6 = {
/** @type {typeof __VLS_5.'update:modelValue'} */
'onUpdate:modelValue': (...[$event]) => {
return (__VLS_ctx.emit('update:modelValue', $event));
// @ts-ignore
[resolvedId, resolvedId, label, required, adapter, modelValue, disabled, error, rows, placeholder, emit,];
},
};
const __VLS_7 = {
/** @type {typeof __VLS_5.blur} */
onBlur: (...[$event]) => {
return (__VLS_ctx.emit('blur', $event));
// @ts-ignore
[emit,];
},
};
var __VLS_3;
var __VLS_4;
if (__VLS_ctx.error || __VLS_ctx.help) {
__VLS_asFunctionalElement1(__VLS_intrinsics.small, __VLS_intrinsics.small)({
...{ class: ({ 'ks-danger-text': __VLS_ctx.error }) },
});
/** @type {__VLS_StyleScopedClasses['ks-danger-text']} */ ;
(__VLS_ctx.error ?? __VLS_ctx.help);
var __VLS_5;
{
const { default: __VLS_6 } = __VLS_3.slots;
const [field] = __VLS_vSlot(__VLS_6);
const __VLS_7 = (__VLS_ctx.adapter.components.TextArea);
// @ts-ignore
const __VLS_8 = __VLS_asFunctionalComponent1(__VLS_7, new __VLS_7({
...{ 'onUpdate:modelValue': {} },
...{ 'onBlur': {} },
inputId: (field.inputId),
modelValue: (__VLS_ctx.modelValue),
disabled: (__VLS_ctx.disabled),
invalid: (field.invalid),
rows: (__VLS_ctx.rows),
placeholder: (__VLS_ctx.placeholder),
'aria-describedby': (field.describedBy),
'aria-required': (field.required || undefined),
}));
const __VLS_9 = __VLS_8({
...{ 'onUpdate:modelValue': {} },
...{ 'onBlur': {} },
inputId: (field.inputId),
modelValue: (__VLS_ctx.modelValue),
disabled: (__VLS_ctx.disabled),
invalid: (field.invalid),
rows: (__VLS_ctx.rows),
placeholder: (__VLS_ctx.placeholder),
'aria-describedby': (field.describedBy),
'aria-required': (field.required || undefined),
}, ...__VLS_functionalComponentArgsRest(__VLS_8));
let __VLS_12;
const __VLS_13 = {
/** @type {typeof __VLS_12.'update:modelValue'} */
'onUpdate:modelValue': (...[$event]) => {
return (__VLS_ctx.emit('update:modelValue', $event));
// @ts-ignore
[label, inputId, required, error, help, adapter, modelValue, disabled, rows, placeholder, emit,];
},
};
const __VLS_14 = {
/** @type {typeof __VLS_12.blur} */
onBlur: (...[$event]) => {
return (__VLS_ctx.emit('blur', $event));
// @ts-ignore
[emit,];
},
};
var __VLS_10;
var __VLS_11;
// @ts-ignore
[];
__VLS_3.slots['' /* empty slot name completion */];
}
var __VLS_3;
// @ts-ignore
[error, error, error, help, help,];
[];
const __VLS_export = (await import('vue')).defineComponent({
__typeEmits: {},
__typeProps: {},
@@ -1,34 +1,25 @@
<script setup lang="ts">
import { computed, useId } from 'vue'
import { useUiAdapter } from '../adapter/useUiAdapter'
import FieldShell from './FieldShell.vue'
const props = defineProps<{ modelValue: string; label: string; inputId?: string; disabled?: boolean; required?: boolean; error?: string; help?: string; placeholder?: string }>()
const emit = defineEmits<{ 'update:modelValue': [value: string]; blur: [event: FocusEvent] }>()
const adapter = useUiAdapter()
const generatedId = useId()
const resolvedId = computed(() => props.inputId ?? `ks-field-${generatedId}`)
</script>
<template>
<div class="ks-field">
<label :for="resolvedId">{{ label }} <span v-if="required" aria-hidden="true">*</span></label>
<FieldShell :label="label" :input-id="inputId" :required="required" :error="error" :help="help" v-slot="field">
<component
:is="adapter.components.TextField"
:input-id="resolvedId"
:input-id="field.inputId"
:model-value="modelValue"
:disabled="disabled"
:invalid="Boolean(error)"
:invalid="field.invalid"
:placeholder="placeholder"
:aria-describedby="error || help ? `${resolvedId}-message` : undefined"
:aria-describedby="field.describedBy"
:aria-required="field.required || undefined"
@update:model-value="emit('update:modelValue', $event)"
@blur="emit('blur', $event)"
/>
<small v-if="error || help" :id="`${resolvedId}-message`" :class="{ 'ks-danger-text': error }">{{ error ?? help }}</small>
</div>
</FieldShell>
</template>
<style scoped>
.ks-field { display: grid; gap: var(--ks-space-1); }
label { font-weight: 650; }
small { color: var(--ks-color-neutral-600); }
</style>
@@ -1,10 +1,8 @@
import { computed, useId } from 'vue';
import { useUiAdapter } from '../adapter/useUiAdapter';
import FieldShell from './FieldShell.vue';
const props = defineProps();
const emit = defineEmits();
const adapter = useUiAdapter();
const generatedId = useId();
const resolvedId = computed(() => props.inputId ?? `ks-field-${generatedId}`);
const __VLS_ctx = {
...{},
...{},
@@ -15,70 +13,76 @@ const __VLS_ctx = {
let __VLS_components;
let __VLS_intrinsics;
let __VLS_directives;
__VLS_asFunctionalElement1(__VLS_intrinsics.div, __VLS_intrinsics.div)({
...{ class: "ks-field" },
});
/** @type {__VLS_StyleScopedClasses['ks-field']} */ ;
__VLS_asFunctionalElement1(__VLS_intrinsics.label, __VLS_intrinsics.label)({
for: (__VLS_ctx.resolvedId),
});
(__VLS_ctx.label);
if (__VLS_ctx.required) {
__VLS_asFunctionalElement1(__VLS_intrinsics.span, __VLS_intrinsics.span)({
'aria-hidden': "true",
});
}
const __VLS_0 = (__VLS_ctx.adapter.components.TextField);
const __VLS_0 = FieldShell || FieldShell;
// @ts-ignore
const __VLS_1 = __VLS_asFunctionalComponent1(__VLS_0, new __VLS_0({
...{ 'onUpdate:modelValue': {} },
...{ 'onBlur': {} },
inputId: (__VLS_ctx.resolvedId),
modelValue: (__VLS_ctx.modelValue),
disabled: (__VLS_ctx.disabled),
invalid: (Boolean(__VLS_ctx.error)),
placeholder: (__VLS_ctx.placeholder),
'aria-describedby': (__VLS_ctx.error || __VLS_ctx.help ? `${__VLS_ctx.resolvedId}-message` : undefined),
label: (__VLS_ctx.label),
inputId: (__VLS_ctx.inputId),
required: (__VLS_ctx.required),
error: (__VLS_ctx.error),
help: (__VLS_ctx.help),
}));
const __VLS_2 = __VLS_1({
...{ 'onUpdate:modelValue': {} },
...{ 'onBlur': {} },
inputId: (__VLS_ctx.resolvedId),
modelValue: (__VLS_ctx.modelValue),
disabled: (__VLS_ctx.disabled),
invalid: (Boolean(__VLS_ctx.error)),
placeholder: (__VLS_ctx.placeholder),
'aria-describedby': (__VLS_ctx.error || __VLS_ctx.help ? `${__VLS_ctx.resolvedId}-message` : undefined),
label: (__VLS_ctx.label),
inputId: (__VLS_ctx.inputId),
required: (__VLS_ctx.required),
error: (__VLS_ctx.error),
help: (__VLS_ctx.help),
}, ...__VLS_functionalComponentArgsRest(__VLS_1));
let __VLS_5;
const __VLS_6 = {
/** @type {typeof __VLS_5.'update:modelValue'} */
'onUpdate:modelValue': (...[$event]) => {
return (__VLS_ctx.emit('update:modelValue', $event));
// @ts-ignore
[resolvedId, resolvedId, resolvedId, label, required, adapter, modelValue, disabled, error, error, placeholder, help, emit,];
},
};
const __VLS_7 = {
/** @type {typeof __VLS_5.blur} */
onBlur: (...[$event]) => {
return (__VLS_ctx.emit('blur', $event));
// @ts-ignore
[emit,];
},
};
var __VLS_3;
var __VLS_4;
if (__VLS_ctx.error || __VLS_ctx.help) {
__VLS_asFunctionalElement1(__VLS_intrinsics.small, __VLS_intrinsics.small)({
id: (`${__VLS_ctx.resolvedId}-message`),
...{ class: ({ 'ks-danger-text': __VLS_ctx.error }) },
});
/** @type {__VLS_StyleScopedClasses['ks-danger-text']} */ ;
(__VLS_ctx.error ?? __VLS_ctx.help);
var __VLS_5;
{
const { default: __VLS_6 } = __VLS_3.slots;
const [field] = __VLS_vSlot(__VLS_6);
const __VLS_7 = (__VLS_ctx.adapter.components.TextField);
// @ts-ignore
const __VLS_8 = __VLS_asFunctionalComponent1(__VLS_7, new __VLS_7({
...{ 'onUpdate:modelValue': {} },
...{ 'onBlur': {} },
inputId: (field.inputId),
modelValue: (__VLS_ctx.modelValue),
disabled: (__VLS_ctx.disabled),
invalid: (field.invalid),
placeholder: (__VLS_ctx.placeholder),
'aria-describedby': (field.describedBy),
'aria-required': (field.required || undefined),
}));
const __VLS_9 = __VLS_8({
...{ 'onUpdate:modelValue': {} },
...{ 'onBlur': {} },
inputId: (field.inputId),
modelValue: (__VLS_ctx.modelValue),
disabled: (__VLS_ctx.disabled),
invalid: (field.invalid),
placeholder: (__VLS_ctx.placeholder),
'aria-describedby': (field.describedBy),
'aria-required': (field.required || undefined),
}, ...__VLS_functionalComponentArgsRest(__VLS_8));
let __VLS_12;
const __VLS_13 = {
/** @type {typeof __VLS_12.'update:modelValue'} */
'onUpdate:modelValue': (...[$event]) => {
return (__VLS_ctx.emit('update:modelValue', $event));
// @ts-ignore
[label, inputId, required, error, help, adapter, modelValue, disabled, placeholder, emit,];
},
};
const __VLS_14 = {
/** @type {typeof __VLS_12.blur} */
onBlur: (...[$event]) => {
return (__VLS_ctx.emit('blur', $event));
// @ts-ignore
[emit,];
},
};
var __VLS_10;
var __VLS_11;
// @ts-ignore
[];
__VLS_3.slots['' /* empty slot name completion */];
}
var __VLS_3;
// @ts-ignore
[resolvedId, error, error, error, help, help,];
[];
const __VLS_export = (await import('vue')).defineComponent({
__typeEmits: {},
__typeProps: {},
@@ -0,0 +1,36 @@
import { h } from 'vue';
import { mount } from '@vue/test-utils';
import { describe, expect, it } from 'vitest';
import FieldShell from '../FieldShell.vue';
describe('FieldShell', () => {
it('connects label, required state, invalid state, and error to the control', () => {
const wrapper = mount(FieldShell, {
props: { label: 'Customer name', inputId: 'customer-name', required: true, error: 'Required field' },
slots: {
default: ({ inputId, describedBy, invalid, required }) => h('input', {
id: inputId,
'aria-describedby': describedBy,
'aria-invalid': invalid,
'aria-required': required
})
}
});
expect(wrapper.get('label').attributes('for')).toBe('customer-name');
expect(wrapper.get('input').attributes()).toMatchObject({
id: 'customer-name',
'aria-describedby': 'customer-name-message',
'aria-invalid': 'true',
'aria-required': 'true'
});
expect(wrapper.get('[role="alert"]').attributes('id')).toBe('customer-name-message');
expect(wrapper.text()).toContain('Required field');
});
it('uses help text without announcing it as an error', () => {
const wrapper = mount(FieldShell, {
props: { label: 'Settlement date', inputId: 'settlement-date', help: 'Use the market calendar date.' },
slots: { default: ({ inputId, describedBy }) => h('input', { id: inputId, 'aria-describedby': describedBy }) }
});
expect(wrapper.get('small').attributes()).toMatchObject({ id: 'settlement-date-message' });
expect(wrapper.find('[role="alert"]').exists()).toBe(false);
});
});
@@ -0,0 +1,40 @@
import { h } from 'vue'
import { mount } from '@vue/test-utils'
import { describe, expect, it } from 'vitest'
import FieldShell from '../FieldShell.vue'
describe('FieldShell', () => {
it('connects label, required state, invalid state, and error to the control', () => {
const wrapper = mount(FieldShell, {
props: { label: 'Customer name', inputId: 'customer-name', required: true, error: 'Required field' },
slots: {
default: ({ inputId, describedBy, invalid, required }) => h('input', {
id: inputId,
'aria-describedby': describedBy,
'aria-invalid': invalid,
'aria-required': required
})
}
})
expect(wrapper.get('label').attributes('for')).toBe('customer-name')
expect(wrapper.get('input').attributes()).toMatchObject({
id: 'customer-name',
'aria-describedby': 'customer-name-message',
'aria-invalid': 'true',
'aria-required': 'true'
})
expect(wrapper.get('[role="alert"]').attributes('id')).toBe('customer-name-message')
expect(wrapper.text()).toContain('Required field')
})
it('uses help text without announcing it as an error', () => {
const wrapper = mount(FieldShell, {
props: { label: 'Settlement date', inputId: 'settlement-date', help: 'Use the market calendar date.' },
slots: { default: ({ inputId, describedBy }) => h('input', { id: inputId, 'aria-describedby': describedBy }) }
})
expect(wrapper.get('small').attributes()).toMatchObject({ id: 'settlement-date-message' })
expect(wrapper.find('[role="alert"]').exists()).toBe(false)
})
})