diff --git a/.jules/palette.md b/.jules/palette.md index 2dcd639e..f9502290 100644 --- a/.jules/palette.md +++ b/.jules/palette.md @@ -1,3 +1,7 @@ +## 2024-08-09 - Expanding file drop zones for click events +**Learning:** Tiny file input buttons are hard targets. Expanding the file drop zone containers to trigger the file input on click greatly improves accessibility and usability. +**Action:** When expanding file drop zones to wrap form containers, add a click event listener to programmatically trigger the hidden file input's `.click()` method, while explicitly ignoring clicks on interactive child elements (e.g., `['INPUT', 'BUTTON', 'LABEL'].includes(e.target.tagName)`). + ## 2024-07-15 - Dynamic Size formatting and Total Size Validation **Learning:** Hardcoding human-readable sizes (like '5 GiB') in validation error messages is error-prone when the underlying constant changes. Moreover, failing to validate total upload size against backend limits (e.g., MAX_UPLOAD_BYTES) in batch file uploads frustrates users who wait for a large upload to finish only to get a server-side 413 Payload Too Large error. **Action:** Always format backend byte limit constants dynamically (e.g., `formatBinaryBytes(MAX_UPLOAD_BYTES)`) on the client side to display accurate error messages. For multiple file inputs, ensure both the file count and the combined file size are validated against backend limits, giving immediate inline feedback via `setCustomValidity` and `aria-invalid`. diff --git a/CHANGELOG.md b/CHANGELOG.md index 4cca1ced..5e4dff70 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,3 +11,6 @@ ### Fixed - 단일·일괄 대상 크기 입력을 비웠을 때 이전 custom validity와 `aria-invalid` 상태를 즉시 초기화해 현재 필수 입력 상태를 정확히 전달합니다. + +### 🎨 Palette: [UX improvement] 파일 업로드 드롭 존 영역 클릭 타겟 확장 +- 파일 업로드 드롭 존 영역을 클릭하여 파일 선택 창을 열 수 있도록 개선하여 접근성과 사용성을 향상시켰습니다. diff --git a/saas_web.py b/saas_web.py index 0ef95a1e..f5d54379 100644 --- a/saas_web.py +++ b/saas_web.py @@ -385,6 +385,18 @@ async def add_security_headers(request: Request, call_next): e.preventDefault(); e.stopPropagation(); } + dropZone.addEventListener('click', (e) => { + if (!['INPUT', 'BUTTON', 'LABEL'].includes(e.target.tagName)) { + fileInput.click(); + } + }); + if (batchDropZone) { + batchDropZone.addEventListener('click', (e) => { + if (!['INPUT', 'BUTTON', 'LABEL'].includes(e.target.tagName)) { + batchFileInput.click(); + } + }); + } dropZone.addEventListener('drop', (e) => { let dt = e.dataTransfer; let files = dt.files; diff --git a/tests/test_saas_web.py b/tests/test_saas_web.py index cd45dbc3..ddc07bed 100644 --- a/tests/test_saas_web.py +++ b/tests/test_saas_web.py @@ -333,6 +333,9 @@ def test_get_ui_includes_preset_buttons(self): self.assertIn('aria-pressed="false"', html) self.assertIn('role="group" aria-label="Preset target sizes"', html) self.assertNotIn('onclick="setTargetBytes(', html) + self.assertIn("dropZone.addEventListener('click'", html) + self.assertIn("batchDropZone.addEventListener('click'", html) + self.assertIn("!['INPUT', 'BUTTON', 'LABEL'].includes(e.target.tagName)", html) self.assertIn("const presetValue = Number.parseInt(btn.dataset.bytes, 10);", html) self.assertIn("!e.isTrusted && presetValue === val", html) self.assertNotIn("btn.dataset.bytes === this.value", html)