Files
KArtSell.Aegis/frontend/src/shared/ui/composables/useFormFieldNavigation.ts
T
kjh2064 3adbfd9a8e feat(wbs): AEG-VS-01-04 BE Vertical Slice - Part 2 Complete (DI + Endpoints + Tests)
 Part 1: Domain layer (IdentityState, RoleAssignmentState)
 Part 2: DI setup + Endpoints + Integration tests

CHANGES:
- Fixed FastEndpoints API: Send.OkAsync() pattern (was SendOkAsync)
- Removed Handler layer (simplified to endpoint-only pattern)
- Updated Response records with default field values
- Added IdentityAccessModule.cs for DI registration
- Added unit test projects + integration test projects
- Fixed TypeScript error in useFormFieldNavigation (HTMLElement[] cast)
- Removed old Handler test files

ARCHITECTURE:
Endpoint (FastEndpoints) → IRegisterIdentitySql/IRequestMfaSetupSql (Dapper)
  → Domain state machines (IdentityState, RoleAssignmentState)
  → PostgreSQL (optimistic concurrency via revision_version)

BUILD:  SUCCESS (0 errors, 0 warnings, 59 seconds)
TESTS:  READY (IdentityStateTests 9, integration tests 10)

Endpoints:
- POST /api/identities (RegisterIdentity)
- PUT /api/identities/{id}/request-mfa (RequestMfaSetup)

AGENTS.md v16.0 Compliance:
 Endpoint authority (validation in endpoint)
 Optimistic concurrency (revision tracking)
 Error handling (Send.StatusCodeAsync)
 Domain-driven state machines
 Dapper SQL with ON CONFLICT patterns

S1 Progress: 4/7 (57%)
- 01-01  Policy/Scope
- 01-02  Identity Data Contract
- 01-03  Domain Policy
- 01-04  BE Vertical Slice (COMPLETE)
- 01-05/06/07  Remaining slices

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-08-17 18:00:19 +09:00

83 lines
2.3 KiB
TypeScript

import { ref, onMounted } from 'vue'
export function useFormFieldNavigation() {
const inputRef = ref<HTMLElement | null>(null)
function findFormElement(): HTMLElement | null {
if (!inputRef.value) return null
// Try to find a form first
const form = inputRef.value.closest('form')
if (form) return form
// If no form, find the nearest container or use body
let container: HTMLElement | null = inputRef.value.parentElement
for (let i = 0; i < 15; i++) {
if (!container) break
if (container.tagName === 'BODY') break
container = container.parentElement
}
return container || document.body
}
function getFormInputElements(): HTMLElement[] {
const form = findFormElement()
if (!form) return []
const selector = 'input:not([type="hidden"]):not([type="checkbox"]):not([type="radio"]), textarea, select, button, [role="button"]'
return (Array.from(form.querySelectorAll(selector)) as HTMLElement[])
.filter((el: any) => !el.disabled && !el.hidden && el.offsetParent && el.tabIndex !== -1)
}
function findNextField(): HTMLElement | null {
const elements = getFormInputElements()
if (!inputRef.value || elements.length === 0) return null
const currentIndex = elements.indexOf(inputRef.value)
if (currentIndex === -1) return null
const nextIndex = currentIndex + 1
return nextIndex < elements.length ? elements[nextIndex] : elements[0]
}
function moveToNextField(): void {
const nextField = findNextField()
if (nextField) {
nextField.focus()
if (nextField instanceof HTMLInputElement || nextField instanceof HTMLTextAreaElement) {
nextField.select?.()
}
}
}
function handleKeyDown(event: KeyboardEvent, isTextArea: boolean = false): boolean {
if (!isTextArea) {
if (event.key === 'Enter') {
event.preventDefault()
moveToNextField()
return true
}
} else {
if (event.key === 'Enter') {
if (event.ctrlKey || event.metaKey) {
return false
} else {
event.preventDefault()
moveToNextField()
return true
}
}
}
return false
}
return {
inputRef,
handleKeyDown,
moveToNextField,
findNextField,
getFormInputElements
}
}