feat: Implement validation error message display for required fields
- Added a new CSS class for displaying validation error messages below required input fields without affecting layout. - Enhanced the `useDialogAutoValidation` hook to insert error messages dynamically when required fields are empty. - Implemented logic to remove error messages when the input is cleared, improving user feedback during form validation. Made-with: Cursor
This commit is contained in:
parent
35dfe5bd79
commit
cfd49020a0
|
|
@ -441,4 +441,21 @@ select {
|
||||||
border-color: hsl(var(--destructive)) !important;
|
border-color: hsl(var(--destructive)) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 필수 입력 경고 문구 (입력 필드 아래, 레이아웃 영향 없음) */
|
||||||
|
.validation-error-msg-wrapper {
|
||||||
|
height: 0;
|
||||||
|
overflow: visible;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.validation-error-msg-wrapper > p {
|
||||||
|
position: absolute;
|
||||||
|
top: 1px;
|
||||||
|
left: 0;
|
||||||
|
font-size: 11px;
|
||||||
|
color: hsl(var(--destructive));
|
||||||
|
white-space: nowrap;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
/* ===== End of Global Styles ===== */
|
/* ===== End of Global Styles ===== */
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,6 @@ const DRAG_THRESHOLD = 5;
|
||||||
const SETTLE_MS = 70;
|
const SETTLE_MS = 70;
|
||||||
const DROP_SETTLE_MS = 180;
|
const DROP_SETTLE_MS = 180;
|
||||||
const BAR_PAD_X = 8;
|
const BAR_PAD_X = 8;
|
||||||
const ACTIVE_TAB_BORDER_OPACITY = 0.3;
|
|
||||||
|
|
||||||
interface DragState {
|
interface DragState {
|
||||||
tabId: string;
|
tabId: string;
|
||||||
|
|
@ -502,7 +501,7 @@ export function TabBar() {
|
||||||
touchAction: "none",
|
touchAction: "none",
|
||||||
...animStyle,
|
...animStyle,
|
||||||
...(hiddenByGhost ? { opacity: 0 } : {}),
|
...(hiddenByGhost ? { opacity: 0 } : {}),
|
||||||
...(isActive ? { borderColor: `rgba(0,0,0,${ACTIVE_TAB_BORDER_OPACITY})` } : {}),
|
...(isActive ? { boxShadow: "0 -1px 28px rgba(0,0,0,0.9)" } : {}),
|
||||||
}}
|
}}
|
||||||
title={tab.title}
|
title={tab.title}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import { toast } from "sonner";
|
||||||
|
|
||||||
const HIGHLIGHT_ATTR = "data-validation-highlight";
|
const HIGHLIGHT_ATTR = "data-validation-highlight";
|
||||||
const ERROR_ATTR = "data-validation-error";
|
const ERROR_ATTR = "data-validation-error";
|
||||||
|
const MSG_WRAPPER_CLASS = "validation-error-msg-wrapper";
|
||||||
|
|
||||||
type TargetEl = HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement | HTMLButtonElement;
|
type TargetEl = HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement | HTMLButtonElement;
|
||||||
|
|
||||||
|
|
@ -121,11 +122,32 @@ export function useDialogAutoValidation(contentEl: HTMLElement | null) {
|
||||||
function markError(input: TargetEl) {
|
function markError(input: TargetEl) {
|
||||||
input.setAttribute(ERROR_ATTR, "true");
|
input.setAttribute(ERROR_ATTR, "true");
|
||||||
errorFields.add(input);
|
errorFields.add(input);
|
||||||
|
showErrorMsg(input);
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearError(input: TargetEl) {
|
function clearError(input: TargetEl) {
|
||||||
input.removeAttribute(ERROR_ATTR);
|
input.removeAttribute(ERROR_ATTR);
|
||||||
errorFields.delete(input);
|
errorFields.delete(input);
|
||||||
|
removeErrorMsg(input);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 빈 필수 필드 아래에 경고 문구 삽입 (레이아웃 영향 없는 zero-height wrapper)
|
||||||
|
function showErrorMsg(input: TargetEl) {
|
||||||
|
if (input.parentElement?.querySelector(`.${MSG_WRAPPER_CLASS}`)) return;
|
||||||
|
|
||||||
|
const wrapper = document.createElement("div");
|
||||||
|
wrapper.className = MSG_WRAPPER_CLASS;
|
||||||
|
|
||||||
|
const msg = document.createElement("p");
|
||||||
|
msg.textContent = "필수 입력 항목입니다";
|
||||||
|
wrapper.appendChild(msg);
|
||||||
|
|
||||||
|
input.insertAdjacentElement("afterend", wrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeErrorMsg(input: TargetEl) {
|
||||||
|
const wrapper = input.parentElement?.querySelector(`.${MSG_WRAPPER_CLASS}`);
|
||||||
|
if (wrapper) wrapper.remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
function highlightField(input: TargetEl) {
|
function highlightField(input: TargetEl) {
|
||||||
|
|
@ -200,6 +222,7 @@ export function useDialogAutoValidation(contentEl: HTMLElement | null) {
|
||||||
|
|
||||||
el.querySelectorAll(`[${HIGHLIGHT_ATTR}]`).forEach((node) => node.removeAttribute(HIGHLIGHT_ATTR));
|
el.querySelectorAll(`[${HIGHLIGHT_ATTR}]`).forEach((node) => node.removeAttribute(HIGHLIGHT_ATTR));
|
||||||
el.querySelectorAll(`[${ERROR_ATTR}]`).forEach((node) => node.removeAttribute(ERROR_ATTR));
|
el.querySelectorAll(`[${ERROR_ATTR}]`).forEach((node) => node.removeAttribute(ERROR_ATTR));
|
||||||
|
el.querySelectorAll(`.${MSG_WRAPPER_CLASS}`).forEach((node) => node.remove());
|
||||||
};
|
};
|
||||||
}, [mode, contentEl]);
|
}, [mode, contentEl]);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue